seo 0.2.19 → 0.2.21

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as load, t as version } from "./package-Dl66IhTH.js";
2
+ import { n as version, t as load } from "./esm-CSE-hgHf.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -112,6 +112,88 @@ function seoErrorEnvelope(error) {
112
112
  };
113
113
  }
114
114
  //#endregion
115
+ //#region packages/core/src/version.ts
116
+ const SEO_VERSION = version;
117
+ //#endregion
118
+ //#region packages/core/src/fetch/crawler-identity.ts
119
+ const SEO_CRAWLER_TOKEN = "SEO-Skill";
120
+ const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
121
+ const SEO_CRAWLER_IDENTITY = {
122
+ name: "SEO Skill",
123
+ robotsToken: SEO_CRAWLER_TOKEN,
124
+ version: SEO_VERSION,
125
+ userAgent: SEO_CRAWLER_USER_AGENT,
126
+ documentationUrl: "https://seoskill.dev/docs/crawler/"
127
+ };
128
+ //#endregion
129
+ //#region packages/core/src/fetch/http-client.ts
130
+ var ResponseSizeLimitError = class extends Error {
131
+ maxBytes;
132
+ constructor(maxBytes, label = "Response") {
133
+ super(`${label} exceeds the ${maxBytes}-byte response limit.`);
134
+ this.maxBytes = maxBytes;
135
+ this.name = "ResponseSizeLimitError";
136
+ }
137
+ };
138
+ /** @deprecated Use SEO_CRAWLER_USER_AGENT. */
139
+ const BROWSER_USER_AGENT = SEO_CRAWLER_USER_AGENT;
140
+ function requestHeaders(profile) {
141
+ if (profile === "bot") return new Headers({
142
+ accept: "text/plain,*/*;q=0.8",
143
+ "accept-language": "en-US,en;q=0.9",
144
+ connection: "close",
145
+ "user-agent": SEO_CRAWLER_USER_AGENT
146
+ });
147
+ return new Headers({
148
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
149
+ "accept-language": "en-US,en;q=0.9",
150
+ connection: "close",
151
+ "sec-ch-ua-mobile": "?0",
152
+ "sec-ch-ua-platform": "\"macOS\"",
153
+ "upgrade-insecure-requests": "1",
154
+ "user-agent": SEO_CRAWLER_USER_AGENT
155
+ });
156
+ }
157
+ function publicHttpFetch(url, input = {}) {
158
+ const { profile = "browser", headers, ...init } = input;
159
+ const base = requestHeaders(profile);
160
+ if (headers instanceof Headers) for (const [key, value] of headers.entries()) base.set(key, value);
161
+ else if (Array.isArray(headers)) for (const [key, value] of headers) base.set(key, value);
162
+ else if (headers) {
163
+ for (const [key, value] of Object.entries(headers)) if (value !== void 0) base.set(key, value);
164
+ }
165
+ return fetch$1(url, {
166
+ ...init,
167
+ headers: base
168
+ });
169
+ }
170
+ async function readBoundedResponseText(response, maxBytes, label = "Response") {
171
+ const declaredLength = Number(response.headers.get("content-length"));
172
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
173
+ await response.body?.cancel().catch(() => void 0);
174
+ throw new ResponseSizeLimitError(maxBytes, label);
175
+ }
176
+ const reader = response.body?.getReader();
177
+ if (!reader) return "";
178
+ const chunks = [];
179
+ let total = 0;
180
+ try {
181
+ while (true) {
182
+ const { done, value } = await reader.read();
183
+ if (done) break;
184
+ total += value.byteLength;
185
+ if (total > maxBytes) {
186
+ await reader.cancel().catch(() => void 0);
187
+ throw new ResponseSizeLimitError(maxBytes, label);
188
+ }
189
+ chunks.push(value);
190
+ }
191
+ } finally {
192
+ reader.releaseLock();
193
+ }
194
+ return Buffer.concat(chunks, total).toString("utf8");
195
+ }
196
+ //#endregion
115
197
  //#region packages/core/src/paths.ts
116
198
  let memoizedPaths;
117
199
  function defaultConfigDir(input) {
@@ -1925,8 +2007,48 @@ function clearCache(provider, olderThanMs) {
1925
2007
  compactCacheDatabase(database, { allowFullVacuum: true });
1926
2008
  return removed;
1927
2009
  }
2010
+ const propertySummarySchema = z.object({
2011
+ property: z.string().trim().min(1),
2012
+ displayName: z.string().optional(),
2013
+ propertyType: z.string().optional(),
2014
+ parent: z.string().optional()
2015
+ }).strip();
2016
+ const accountSummaryPageSchema = z.object({
2017
+ accountSummaries: z.array(z.object({
2018
+ account: z.string().trim().min(1),
2019
+ displayName: z.string().optional(),
2020
+ propertySummaries: z.array(propertySummarySchema).optional()
2021
+ }).strip()).max(200).optional(),
2022
+ nextPageToken: z.string().optional()
2023
+ }).strip();
2024
+ function parseGa4AccountSummaryPage(value) {
2025
+ const parsed = accountSummaryPageSchema.safeParse(value);
2026
+ if (!parsed.success) throw new Error("Google Analytics account summary response was invalid.");
2027
+ return {
2028
+ accountSummaries: (parsed.data.accountSummaries ?? []).map((account) => ({
2029
+ ...account,
2030
+ propertySummaries: account.propertySummaries ?? []
2031
+ })),
2032
+ nextPageToken: parsed.data.nextPageToken || void 0
2033
+ };
2034
+ }
2035
+ async function collectGa4AccountSummaries(fetchPage) {
2036
+ const summaries = [];
2037
+ const seenPageTokens = /* @__PURE__ */ new Set();
2038
+ let pageToken;
2039
+ for (let page = 0; page < 20; page += 1) {
2040
+ const result = parseGa4AccountSummaryPage(await fetchPage(pageToken));
2041
+ summaries.push(...result.accountSummaries);
2042
+ if (!result.nextPageToken) return summaries;
2043
+ if (seenPageTokens.has(result.nextPageToken)) throw new Error("Google Analytics account summary pagination repeated a page token.");
2044
+ seenPageTokens.add(result.nextPageToken);
2045
+ pageToken = result.nextPageToken;
2046
+ }
2047
+ throw new Error(`Google Analytics account summary discovery exceeded 20 pages.`);
2048
+ }
1928
2049
  //#endregion
1929
2050
  //#region packages/core/src/ga4/client.ts
2051
+ const GA4_ACCOUNT_SUMMARY_RESPONSE_LIMIT_BYTES = 10 * 1024 * 1024;
1930
2052
  function ga4RequestCanUseCache(body) {
1931
2053
  return body.dateRanges.every((range) => /^\d{4}-\d{2}-\d{2}$/.test(range.startDate) && /^\d{4}-\d{2}-\d{2}$/.test(range.endDate));
1932
2054
  }
@@ -1971,12 +2093,22 @@ async function runGa4Report(propertyId, body, opts = {}) {
1971
2093
  }
1972
2094
  async function listGa4AccountSummaries() {
1973
2095
  const { client } = await createGoogleAccessTokenClient();
1974
- const response = await authedFetch$1(client, "https://analyticsadmin.googleapis.com/v1beta/accountSummaries");
1975
- if (!response.ok) {
1976
- if (response.status === 403) throw new Error("Google Analytics account summary fetch failed with 403. Enable the Google Analytics Admin API and check Analytics access.");
1977
- throw new Error(`Google Analytics account summary fetch failed with ${response.status}.`);
1978
- }
1979
- return (await response.json()).accountSummaries ?? [];
2096
+ return collectGa4AccountSummaries(async (pageToken) => {
2097
+ const url = new URL("https://analyticsadmin.googleapis.com/v1beta/accountSummaries");
2098
+ url.searchParams.set("pageSize", "200");
2099
+ if (pageToken) url.searchParams.set("pageToken", pageToken);
2100
+ const response = await authedFetch$1(client, url.toString());
2101
+ if (!response.ok) {
2102
+ if (response.status === 403) throw new Error("Google Analytics account summary fetch failed with 403. Enable the Google Analytics Admin API and check Analytics access.");
2103
+ throw new Error(`Google Analytics account summary fetch failed with ${response.status}.`);
2104
+ }
2105
+ const text = await readBoundedResponseText(response, GA4_ACCOUNT_SUMMARY_RESPONSE_LIMIT_BYTES, "Google Analytics account summary response");
2106
+ try {
2107
+ return JSON.parse(text);
2108
+ } catch {
2109
+ throw new Error("Google Analytics account summary response was invalid.");
2110
+ }
2111
+ });
1980
2112
  }
1981
2113
  async function listGa4DataStreams(propertyId) {
1982
2114
  const { client } = await createGoogleAccessTokenClient();
@@ -2120,7 +2252,7 @@ function aiReferralSourceForValue(value) {
2120
2252
  function compareText$18(left, right) {
2121
2253
  return left < right ? -1 : left > right ? 1 : 0;
2122
2254
  }
2123
- function metric(row, name) {
2255
+ function metric$1(row, name) {
2124
2256
  const value = row[name];
2125
2257
  const parsed = Number(value);
2126
2258
  if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
@@ -2155,8 +2287,8 @@ function analyzeAiReferralRows(input) {
2155
2287
  ...emptyMetrics()
2156
2288
  };
2157
2289
  addMetrics(total, {
2158
- sessions: metric(row, "sessions"),
2159
- eventCount: metric(row, "eventCount")
2290
+ sessions: metric$1(row, "sessions"),
2291
+ eventCount: metric$1(row, "eventCount")
2160
2292
  });
2161
2293
  total.observedSessionSources.add(row.sessionSource ?? "");
2162
2294
  sourceTotals.set(definition.id, total);
@@ -2168,8 +2300,8 @@ function analyzeAiReferralRows(input) {
2168
2300
  const definition = aiReferralSourceForValue(row.sessionSource ?? "");
2169
2301
  if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
2170
2302
  const metrics = {
2171
- sessions: metric(row, "sessions"),
2172
- eventCount: metric(row, "eventCount")
2303
+ sessions: metric$1(row, "sessions"),
2304
+ eventCount: metric$1(row, "eventCount")
2173
2305
  };
2174
2306
  addMetrics(detailTotal, metrics);
2175
2307
  const landingPage = row.landingPagePlusQueryString || "(not set)";
@@ -3812,20 +3944,6 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
3812
3944
  };
3813
3945
  }
3814
3946
  //#endregion
3815
- //#region packages/core/src/version.ts
3816
- const SEO_VERSION = version;
3817
- //#endregion
3818
- //#region packages/core/src/fetch/crawler-identity.ts
3819
- const SEO_CRAWLER_TOKEN = "SEO-Skill";
3820
- const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
3821
- const SEO_CRAWLER_IDENTITY = {
3822
- name: "SEO Skill",
3823
- robotsToken: SEO_CRAWLER_TOKEN,
3824
- version: SEO_VERSION,
3825
- userAgent: SEO_CRAWLER_USER_AGENT,
3826
- documentationUrl: "https://seoskill.dev/docs/crawler/"
3827
- };
3828
- //#endregion
3829
3947
  //#region packages/core/src/fetch/access-block.ts
3830
3948
  const CLOUDFLARE_SKIP_DOCS = "https://developers.cloudflare.com/waf/custom-rules/skip/";
3831
3949
  function isHeadersLike(headers) {
@@ -3897,74 +4015,6 @@ function detectAccessBlock(input) {
3897
4015
  };
3898
4016
  }
3899
4017
  //#endregion
3900
- //#region packages/core/src/fetch/http-client.ts
3901
- var ResponseSizeLimitError = class extends Error {
3902
- maxBytes;
3903
- constructor(maxBytes, label = "Response") {
3904
- super(`${label} exceeds the ${maxBytes}-byte response limit.`);
3905
- this.maxBytes = maxBytes;
3906
- this.name = "ResponseSizeLimitError";
3907
- }
3908
- };
3909
- /** @deprecated Use SEO_CRAWLER_USER_AGENT. */
3910
- const BROWSER_USER_AGENT = SEO_CRAWLER_USER_AGENT;
3911
- function requestHeaders(profile) {
3912
- if (profile === "bot") return new Headers({
3913
- accept: "text/plain,*/*;q=0.8",
3914
- "accept-language": "en-US,en;q=0.9",
3915
- connection: "close",
3916
- "user-agent": SEO_CRAWLER_USER_AGENT
3917
- });
3918
- return new Headers({
3919
- accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
3920
- "accept-language": "en-US,en;q=0.9",
3921
- connection: "close",
3922
- "sec-ch-ua-mobile": "?0",
3923
- "sec-ch-ua-platform": "\"macOS\"",
3924
- "upgrade-insecure-requests": "1",
3925
- "user-agent": SEO_CRAWLER_USER_AGENT
3926
- });
3927
- }
3928
- function publicHttpFetch(url, input = {}) {
3929
- const { profile = "browser", headers, ...init } = input;
3930
- const base = requestHeaders(profile);
3931
- if (headers instanceof Headers) for (const [key, value] of headers.entries()) base.set(key, value);
3932
- else if (Array.isArray(headers)) for (const [key, value] of headers) base.set(key, value);
3933
- else if (headers) {
3934
- for (const [key, value] of Object.entries(headers)) if (value !== void 0) base.set(key, value);
3935
- }
3936
- return fetch$1(url, {
3937
- ...init,
3938
- headers: base
3939
- });
3940
- }
3941
- async function readBoundedResponseText(response, maxBytes, label = "Response") {
3942
- const declaredLength = Number(response.headers.get("content-length"));
3943
- if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
3944
- await response.body?.cancel().catch(() => void 0);
3945
- throw new ResponseSizeLimitError(maxBytes, label);
3946
- }
3947
- const reader = response.body?.getReader();
3948
- if (!reader) return "";
3949
- const chunks = [];
3950
- let total = 0;
3951
- try {
3952
- while (true) {
3953
- const { done, value } = await reader.read();
3954
- if (done) break;
3955
- total += value.byteLength;
3956
- if (total > maxBytes) {
3957
- await reader.cancel().catch(() => void 0);
3958
- throw new ResponseSizeLimitError(maxBytes, label);
3959
- }
3960
- chunks.push(value);
3961
- }
3962
- } finally {
3963
- reader.releaseLock();
3964
- }
3965
- return Buffer.concat(chunks, total).toString("utf8");
3966
- }
3967
- //#endregion
3968
4018
  //#region packages/core/src/fetch/page-fetcher/rate-controls.ts
3969
4019
  const HOST_QUEUES = /* @__PURE__ */ new Map();
3970
4020
  const HOST_HEALTH = /* @__PURE__ */ new Map();
@@ -5725,7 +5775,7 @@ const AUDIT_PAGE_RULE_IDS = [
5725
5775
  "canonical_outside_head",
5726
5776
  "canonical_mismatch"
5727
5777
  ];
5728
- const defaultDependencies$16 = {
5778
+ const defaultDependencies$17 = {
5729
5779
  fetchPage,
5730
5780
  queryPageMetrics,
5731
5781
  now: () => /* @__PURE__ */ new Date()
@@ -5756,7 +5806,7 @@ function buildRecommendations(report) {
5756
5806
  });
5757
5807
  return recommendations;
5758
5808
  }
5759
- async function auditPage(input, dependencies = defaultDependencies$16) {
5809
+ async function auditPage(input, dependencies = defaultDependencies$17) {
5760
5810
  const fetched = await dependencies.fetchPage(input.url, {
5761
5811
  js: input.js,
5762
5812
  refresh: input.refresh
@@ -7439,7 +7489,7 @@ function pageOpportunityRecommendations(analysis) {
7439
7489
  //#endregion
7440
7490
  //#region packages/core/src/analyze/page-opportunities.ts
7441
7491
  const MAX_BENCHMARK_ROWS = 1e5;
7442
- const defaultDependencies$15 = {
7492
+ const defaultDependencies$16 = {
7443
7493
  searchAnalytics: querySearchAnalytics,
7444
7494
  fetch: fetchPage,
7445
7495
  extract: extractPage,
@@ -7511,7 +7561,7 @@ function reportCaveats$1(input) {
7511
7561
  input.benchmarkTruncated ? `The site-wide benchmark reached its ${MAX_BENCHMARK_ROWS.toLocaleString("en-US")}-row cap, so provenance is complete but the peer sample is truncated.` : ""
7512
7562
  ].filter(Boolean);
7513
7563
  }
7514
- async function pageOpportunitiesReport(input, dependencies = defaultDependencies$15) {
7564
+ async function pageOpportunitiesReport(input, dependencies = defaultDependencies$16) {
7515
7565
  const url = assertUrlMatchesGscProperty(input.site, input.url);
7516
7566
  const days = reportDays$1(input.days);
7517
7567
  const range = defaultDateRange$1(days);
@@ -11308,7 +11358,7 @@ function absoluteHttpUrl(value) {
11308
11358
  return false;
11309
11359
  }
11310
11360
  }
11311
- function urlKey(value) {
11361
+ function urlKey$1(value) {
11312
11362
  if (!value) return void 0;
11313
11363
  try {
11314
11364
  const url = new URL(value);
@@ -11507,7 +11557,7 @@ function auditCrawlPages(pages, opts = {}) {
11507
11557
  for (const page of pages) {
11508
11558
  if (!isAuditableHtmlPage(page)) continue;
11509
11559
  for (const value of [page.url, page.finalUrl]) {
11510
- const key = urlKey(value);
11560
+ const key = urlKey$1(value);
11511
11561
  if (key) pageByUrl.set(key, page);
11512
11562
  }
11513
11563
  }
@@ -11649,7 +11699,7 @@ function auditCrawlPages(pages, opts = {}) {
11649
11699
  canonicalRaw: page.canonicalRaw,
11650
11700
  canonical: page.canonical
11651
11701
  }));
11652
- const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
11702
+ const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
11653
11703
  if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
11654
11704
  canonical: page.canonical,
11655
11705
  nextCanonical: canonicalTarget.canonical,
@@ -11824,7 +11874,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
11824
11874
  "refresh-token"
11825
11875
  ]);
11826
11876
  const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
11827
- function uniqueSorted(values = []) {
11877
+ function uniqueSorted$1(values = []) {
11828
11878
  return [...new Set(values.filter(Boolean))].sort();
11829
11879
  }
11830
11880
  function normalizeFetchRate(input, strategy) {
@@ -11844,13 +11894,13 @@ function normalizeCrawlConfig(input) {
11844
11894
  mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
11845
11895
  strategy,
11846
11896
  ...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
11847
- urls: uniqueSorted(input.urls ?? []),
11897
+ urls: uniqueSorted$1(input.urls ?? []),
11848
11898
  maxPages: input.maxPages ?? 500,
11849
11899
  maxDepth: input.maxDepth ?? 16,
11850
11900
  concurrency,
11851
11901
  timeoutMs: input.timeoutMs ?? 2e4,
11852
- include: uniqueSorted(input.include ?? []),
11853
- exclude: uniqueSorted(input.exclude ?? []),
11902
+ include: uniqueSorted$1(input.include ?? []),
11903
+ exclude: uniqueSorted$1(input.exclude ?? []),
11854
11904
  respectRobots: input.respectRobots ?? true,
11855
11905
  useSitemap: input.useSitemap ?? true,
11856
11906
  checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
@@ -14104,7 +14154,7 @@ async function fetchSitemapUrls(input) {
14104
14154
  }
14105
14155
  //#endregion
14106
14156
  //#region packages/core/src/analyze/workflows/analytics-value.ts
14107
- const defaultDependencies$14 = { runGa4Report };
14157
+ const defaultDependencies$15 = { runGa4Report };
14108
14158
  function normalizePath(value) {
14109
14159
  if (!value || value === "(not set)") return "";
14110
14160
  const [path = ""] = value.split("?");
@@ -14164,7 +14214,7 @@ function landingPageRankingPolicy(input) {
14164
14214
  warnings
14165
14215
  };
14166
14216
  }
14167
- async function fetchLandingPageValues(input, dependencies = defaultDependencies$14) {
14217
+ async function fetchLandingPageValues(input, dependencies = defaultDependencies$15) {
14168
14218
  const retainedRowLimit = input.limit ?? 5e3;
14169
14219
  if (!input.propertyId) return {
14170
14220
  values: /* @__PURE__ */ new Map(),
@@ -14934,7 +14984,7 @@ async function crawlSite(input, dependencies) {
14934
14984
  const REPORT_BASELINE_MAX_PAGES = 100;
14935
14985
  const REPORT_BASELINE_MAX_DEPTH = 4;
14936
14986
  const REPORT_BASELINE_MAX_AGE_MS = 10080 * 60 * 1e3;
14937
- const defaultDependencies$13 = {
14987
+ const defaultDependencies$14 = {
14938
14988
  latestCrawlReport,
14939
14989
  crawlSite,
14940
14990
  saveCrawlReport: (report) => saveCrawlReport(report, crawlReportStore, { retention: "baseline" }),
@@ -14983,7 +15033,7 @@ function crawlInput$1(input) {
14983
15033
  refresh: input.refresh ?? false
14984
15034
  };
14985
15035
  }
14986
- async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$13) {
15036
+ async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$14) {
14987
15037
  if (input.crawl === false) return {
14988
15038
  status: "skipped",
14989
15039
  reason: "Technical crawl evidence was skipped with --no-crawl."
@@ -18564,15 +18614,15 @@ function analyzeCtrUnderperformersFromRows(input) {
18564
18614
  }
18565
18615
  //#endregion
18566
18616
  //#region packages/core/src/analyze/ctr-underperformers.ts
18567
- const MAX_GSC_ROWS$4 = 1e5;
18617
+ const MAX_GSC_ROWS$5 = 1e5;
18568
18618
  function plural$4(count, singular, pluralLabel = `${singular}s`) {
18569
18619
  return count === 1 ? singular : pluralLabel;
18570
18620
  }
18571
- const defaultDependencies$12 = {
18621
+ const defaultDependencies$13 = {
18572
18622
  searchAnalytics: querySearchAnalytics,
18573
18623
  now: () => /* @__PURE__ */ new Date()
18574
18624
  };
18575
- async function ctrUnderperformersReport(input, dependencies = defaultDependencies$12) {
18625
+ async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
18576
18626
  const minImpressions = integerOption$2({
18577
18627
  value: input.minImpressions,
18578
18628
  fallback: 200,
@@ -18594,7 +18644,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18594
18644
  dimensions: ["query", "page"],
18595
18645
  type: "web",
18596
18646
  dataState: "final",
18597
- maxRows: MAX_GSC_ROWS$4
18647
+ maxRows: MAX_GSC_ROWS$5
18598
18648
  }, { refresh: input.refresh });
18599
18649
  const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
18600
18650
  rows: source.rows,
@@ -18605,7 +18655,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18605
18655
  includeBrand: input.includeBrand
18606
18656
  });
18607
18657
  const top = items[0];
18608
- const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$4;
18658
+ const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
18609
18659
  const partialValidation = selection.invalidRows > 0;
18610
18660
  const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
18611
18661
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -18622,7 +18672,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18622
18672
  dataState: "final",
18623
18673
  rowsFetched: source.rowsFetched,
18624
18674
  calls: source.calls,
18625
- maxRows: MAX_GSC_ROWS$4,
18675
+ maxRows: MAX_GSC_ROWS$5,
18626
18676
  possiblyTruncated,
18627
18677
  completeness,
18628
18678
  validation: {
@@ -18652,7 +18702,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18652
18702
  "CTR benchmarks and calculated click shortfalls are directional heuristics, not forecasts or proof that clicks are available. Validate the search intent and live SERP before editing.",
18653
18703
  ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$4(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
18654
18704
  ...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$4(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
18655
- ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$4} was reached, so lower-ranked source rows may be absent.`] : [],
18705
+ ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$5} was reached, so lower-ranked source rows may be absent.`] : [],
18656
18706
  ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$4(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
18657
18707
  ],
18658
18708
  recommendations: top ? [
@@ -19176,7 +19226,7 @@ function unavailableSegmentImpactReport(input) {
19176
19226
  //#endregion
19177
19227
  //#region packages/core/src/analyze/segment-impact.ts
19178
19228
  const DEFAULT_MAX_ROWS = 1e5;
19179
- const defaultDependencies$11 = {
19229
+ const defaultDependencies$12 = {
19180
19230
  searchAnalytics: querySearchAnalytics,
19181
19231
  now: () => /* @__PURE__ */ new Date()
19182
19232
  };
@@ -19216,7 +19266,7 @@ function compareSegmentRows(input) {
19216
19266
  generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
19217
19267
  });
19218
19268
  }
19219
- async function segmentImpact(input, dependencies = defaultDependencies$11) {
19269
+ async function segmentImpact(input, dependencies = defaultDependencies$12) {
19220
19270
  validateSegmentSite(input.site);
19221
19271
  const now = dependencies.now();
19222
19272
  const ranges = resolveSegmentRanges({
@@ -19771,8 +19821,8 @@ const CANNIBAL_MINIMUM_PAGE_SHARE = MINIMUM_PAGE_SHARE;
19771
19821
  const CANNIBAL_MAXIMUM_DOMINANT_SHARE = MAXIMUM_DOMINANT_SHARE;
19772
19822
  //#endregion
19773
19823
  //#region packages/core/src/analyze/site-diagnostics/cannibal.ts
19774
- const MAX_GSC_ROWS$3 = 1e5;
19775
- const defaultDependencies$10 = {
19824
+ const MAX_GSC_ROWS$4 = 1e5;
19825
+ const defaultDependencies$11 = {
19776
19826
  searchAnalytics: querySearchAnalytics,
19777
19827
  now: () => /* @__PURE__ */ new Date()
19778
19828
  };
@@ -19785,7 +19835,7 @@ function verdict$4(input) {
19785
19835
  const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
19786
19836
  return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
19787
19837
  }
19788
- async function cannibalReport(input, dependencies = defaultDependencies$10) {
19838
+ async function cannibalReport(input, dependencies = defaultDependencies$11) {
19789
19839
  const days = integerOption$2({
19790
19840
  value: input.days,
19791
19841
  fallback: 28,
@@ -19815,7 +19865,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19815
19865
  ...range,
19816
19866
  type: "web",
19817
19867
  dataState: "final",
19818
- maxRows: MAX_GSC_ROWS$3
19868
+ maxRows: MAX_GSC_ROWS$4
19819
19869
  };
19820
19870
  const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
19821
19871
  ...requestBase,
@@ -19838,8 +19888,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19838
19888
  brandTerms: input.brandTerms,
19839
19889
  includeBrand: input.includeBrand
19840
19890
  });
19841
- const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$3;
19842
- const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$3;
19891
+ const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$4;
19892
+ const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$4;
19843
19893
  const possiblyTruncated = pageTruncated || propertyTruncated;
19844
19894
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
19845
19895
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -19864,7 +19914,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19864
19914
  invalidRows: analysis.selection.invalidRows
19865
19915
  },
19866
19916
  calls: pageExposure.calls,
19867
- maxRows: MAX_GSC_ROWS$3,
19917
+ maxRows: MAX_GSC_ROWS$4,
19868
19918
  possiblyTruncated: pageTruncated
19869
19919
  },
19870
19920
  propertyDemand: {
@@ -19876,7 +19926,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19876
19926
  invalidRows: analysis.selection.propertyInvalidRows
19877
19927
  },
19878
19928
  calls: propertyDemand.calls,
19879
- maxRows: MAX_GSC_ROWS$3,
19929
+ maxRows: MAX_GSC_ROWS$4,
19880
19930
  possiblyTruncated: propertyTruncated
19881
19931
  },
19882
19932
  completeness
@@ -20184,8 +20234,8 @@ function analyzeDecay(input) {
20184
20234
  }
20185
20235
  //#endregion
20186
20236
  //#region packages/core/src/analyze/site-diagnostics/decaying.ts
20187
- const MAX_GSC_ROWS$2 = 1e5;
20188
- const defaultDependencies$9 = {
20237
+ const MAX_GSC_ROWS$3 = 1e5;
20238
+ const defaultDependencies$10 = {
20189
20239
  searchAnalytics: querySearchAnalytics,
20190
20240
  now: () => /* @__PURE__ */ new Date()
20191
20241
  };
@@ -20196,7 +20246,7 @@ function verdict$3(report) {
20196
20246
  if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
20197
20247
  return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
20198
20248
  }
20199
- async function decayingReport(input, dependencies = defaultDependencies$9) {
20249
+ async function decayingReport(input, dependencies = defaultDependencies$10) {
20200
20250
  if (input.comparison !== void 0 && input.comparison !== "previous-period" && input.comparison !== "year-over-year") throw new SeoError("INVALID_INPUT", "comparison must be previous-period or year-over-year.");
20201
20251
  if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.trim().length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
20202
20252
  const days = integerOption$2({
@@ -20249,7 +20299,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20249
20299
  aggregationType: "auto",
20250
20300
  type: "web",
20251
20301
  dataState: "final",
20252
- maxRows: MAX_GSC_ROWS$2
20302
+ maxRows: MAX_GSC_ROWS$3
20253
20303
  };
20254
20304
  const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
20255
20305
  ...request,
@@ -20272,8 +20322,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20272
20322
  brandTerms: input.brandTerms,
20273
20323
  includeBrand: input.includeBrand
20274
20324
  });
20275
- const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$2;
20276
- const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$2;
20325
+ const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$3;
20326
+ const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$3;
20277
20327
  const possiblyTruncated = currentTruncated || previousTruncated;
20278
20328
  const dataStatus = current.rowsFetched === 0 && previous.rowsFetched === 0 ? "empty" : current.rowsFetched === 0 || previous.rowsFetched === 0 || possiblyTruncated || analysis.selection.currentInvalidRows > 0 || analysis.selection.previousInvalidRows > 0 || analysis.selection.currentRowNotRetained > 0 ? "partial" : analysis.selection.eligibleRows === 0 ? "filtered" : "complete";
20279
20329
  const reportBase = {
@@ -20300,13 +20350,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20300
20350
  current: {
20301
20351
  rowsFetched: current.rowsFetched,
20302
20352
  calls: current.calls,
20303
- maxRows: MAX_GSC_ROWS$2,
20353
+ maxRows: MAX_GSC_ROWS$3,
20304
20354
  possiblyTruncated: currentTruncated
20305
20355
  },
20306
20356
  previous: {
20307
20357
  rowsFetched: previous.rowsFetched,
20308
20358
  calls: previous.calls,
20309
- maxRows: MAX_GSC_ROWS$2,
20359
+ maxRows: MAX_GSC_ROWS$3,
20310
20360
  possiblyTruncated: previousTruncated
20311
20361
  },
20312
20362
  completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
@@ -20682,12 +20732,12 @@ function quickWinTemplateSummaries(items) {
20682
20732
  }
20683
20733
  //#endregion
20684
20734
  //#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
20685
- const DEFAULT_DAYS$2 = 28;
20686
- const MAX_DAYS$2 = 548;
20735
+ const DEFAULT_DAYS$3 = 28;
20736
+ const MAX_DAYS$3 = 548;
20687
20737
  const DEFAULT_VERIFY_LIMIT$1 = 5;
20688
20738
  const MAX_VERIFY_LIMIT = 100;
20689
20739
  const MAX_SOURCE_ROWS$3 = 1e5;
20690
- const defaultDependencies$8 = {
20740
+ const defaultDependencies$9 = {
20691
20741
  searchAnalytics: querySearchAnalytics,
20692
20742
  fetch: fetchPage,
20693
20743
  extract: extractPage,
@@ -20706,13 +20756,13 @@ function applyVerification$1(item, coverage) {
20706
20756
  evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
20707
20757
  };
20708
20758
  }
20709
- async function quickWinsReport(input, dependencies = defaultDependencies$8) {
20710
- const explicitRange = explicitDateRange(input, MAX_DAYS$2);
20759
+ async function quickWinsReport(input, dependencies = defaultDependencies$9) {
20760
+ const explicitRange = explicitDateRange(input, MAX_DAYS$3);
20711
20761
  const days = explicitRange?.days ?? integerOption$2({
20712
20762
  value: input.days,
20713
- fallback: DEFAULT_DAYS$2,
20763
+ fallback: DEFAULT_DAYS$3,
20714
20764
  minimum: 1,
20715
- maximum: MAX_DAYS$2,
20765
+ maximum: MAX_DAYS$3,
20716
20766
  label: "Days"
20717
20767
  });
20718
20768
  const verifyLimit = integerOption$2({
@@ -21079,11 +21129,11 @@ function analyzeStrikingDistanceRows(input) {
21079
21129
  }
21080
21130
  //#endregion
21081
21131
  //#region packages/core/src/analyze/striking-distance.ts
21082
- const DEFAULT_DAYS$1 = 28;
21083
- const MAX_DAYS$1 = 548;
21132
+ const DEFAULT_DAYS$2 = 28;
21133
+ const MAX_DAYS$2 = 548;
21084
21134
  const DEFAULT_VERIFY_LIMIT = 5;
21085
21135
  const MAX_SOURCE_ROWS$2 = 1e5;
21086
- const defaultDependencies$7 = {
21136
+ const defaultDependencies$8 = {
21087
21137
  searchAnalytics: querySearchAnalytics,
21088
21138
  verifyContent: verifyQueryContent,
21089
21139
  now: () => /* @__PURE__ */ new Date()
@@ -21093,8 +21143,8 @@ function boundedInteger$3(value, fallback, minimum, maximum) {
21093
21143
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
21094
21144
  }
21095
21145
  function reportDays(value) {
21096
- if (value === void 0) return DEFAULT_DAYS$1;
21097
- if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$1) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$1}.`);
21146
+ if (value === void 0) return DEFAULT_DAYS$2;
21147
+ if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$2) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$2}.`);
21098
21148
  return value;
21099
21149
  }
21100
21150
  function dateRange(days, now) {
@@ -21133,7 +21183,7 @@ function verdict$2(analysis) {
21133
21183
  if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
21134
21184
  return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
21135
21185
  }
21136
- async function strikingDistance(input, dependencies = defaultDependencies$7) {
21186
+ async function strikingDistance(input, dependencies = defaultDependencies$8) {
21137
21187
  const days = reportDays(input.days);
21138
21188
  const generatedAt = dependencies.now().toISOString();
21139
21189
  const range = dateRange(days, new Date(generatedAt));
@@ -21980,7 +22030,7 @@ function emptyQuickWins(input) {
21980
22030
  warnings: []
21981
22031
  };
21982
22032
  }
21983
- const defaultDependencies$6 = {
22033
+ const defaultDependencies$7 = {
21984
22034
  trafficAnomaly,
21985
22035
  updateCorrelation,
21986
22036
  segmentImpact,
@@ -21991,7 +22041,7 @@ const defaultDependencies$6 = {
21991
22041
  };
21992
22042
  async function diagnoseProperty(input, dependencies = {}) {
21993
22043
  const providers = {
21994
- ...defaultDependencies$6,
22044
+ ...defaultDependencies$7,
21995
22045
  ...dependencies
21996
22046
  };
21997
22047
  const limit = input.limit ?? 10;
@@ -22939,6 +22989,42 @@ async function providerRequestJson(input) {
22939
22989
  return parsed.data;
22940
22990
  }
22941
22991
  //#endregion
22992
+ //#region packages/core/src/providers/dataforseo/account-pricing.ts
22993
+ function unitPrice(components) {
22994
+ const prices = components ?? [];
22995
+ const total = (type) => {
22996
+ const matching = prices.filter((item) => item.cost_type === type);
22997
+ return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
22998
+ };
22999
+ return {
23000
+ perRequestMicros: total("per_request"),
23001
+ perResultMicros: total("per_result")
23002
+ };
23003
+ }
23004
+ function dataForSeoAccountPrices(account) {
23005
+ return {
23006
+ keywordOverviewPrice: unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal),
23007
+ keywordDiscoveryPrices: {
23008
+ ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
23009
+ related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
23010
+ suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
23011
+ },
23012
+ domainResearchPrices: {
23013
+ domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
23014
+ rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
23015
+ rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
23016
+ serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
23017
+ },
23018
+ linkPrices: {
23019
+ summary: unitPrice(account.price?.backlinks?.summary?.live?.priority_normal),
23020
+ backlinks: unitPrice(account.price?.backlinks?.backlinks?.live?.priority_normal),
23021
+ referringDomains: unitPrice(account.price?.backlinks?.referring_domains?.live?.priority_normal)
23022
+ },
23023
+ serpLiveAdvancedPrice: unitPrice(account.price?.serp?.live?.advanced?.priority_normal),
23024
+ serpTaskPostPrice: unitPrice(account.price?.serp?.task_post?.priority_normal)
23025
+ };
23026
+ }
23027
+ //#endregion
22942
23028
  //#region packages/core/src/providers/dataforseo/account-schema.ts
22943
23029
  const userDataMoneySchema = z.object({
22944
23030
  total: z.number().finite().nonnegative().optional(),
@@ -22956,6 +23042,11 @@ const priceComponentSchema = z.object({
22956
23042
  const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
22957
23043
  const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
22958
23044
  const userDataPriceSchema = z.object({
23045
+ backlinks: z.object({
23046
+ summary: livePriceSchema.optional(),
23047
+ backlinks: livePriceSchema.optional(),
23048
+ referring_domains: livePriceSchema.optional()
23049
+ }).passthrough().optional(),
22959
23050
  dataforseo_labs: z.object({
22960
23051
  keyword_overview: livePriceSchema.optional(),
22961
23052
  keyword_ideas: livePriceSchema.optional(),
@@ -23013,7 +23104,7 @@ function discoveryNextCursor(response) {
23013
23104
  //#region packages/core/src/providers/dataforseo/schema.ts
23014
23105
  const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
23015
23106
  const nullableNumber$1 = z.number().finite().nullable().optional();
23016
- const nullableCount$1 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
23107
+ const nullableCount$2 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
23017
23108
  const monthlySearchSchema$1 = z.object({
23018
23109
  year: z.number().int(),
23019
23110
  month: z.number().int(),
@@ -23029,7 +23120,7 @@ const keywordOverviewItemSchema = z.object({
23029
23120
  last_updated_time: z.string().trim().min(1).nullable().optional()
23030
23121
  }).nullish(),
23031
23122
  keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
23032
- serp_info: z.object({ se_results_count: nullableCount$1 }).nullish(),
23123
+ serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
23033
23124
  search_intent_info: z.object({ main_intent: z.enum([
23034
23125
  "informational",
23035
23126
  "navigational",
@@ -23102,37 +23193,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
23102
23193
  //#region packages/core/src/providers/dataforseo/domain-schema.ts
23103
23194
  const nullableNumber = z.number().finite().nullable().optional();
23104
23195
  const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
23105
- const nullableCount = z.number().int().nonnegative().nullable().optional();
23196
+ const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
23106
23197
  const rankingMetricsSchema = z.object({
23107
- pos_1: nullableCount,
23108
- pos_2_3: nullableCount,
23109
- pos_4_10: nullableCount,
23110
- pos_11_20: nullableCount,
23111
- pos_21_30: nullableCount,
23112
- pos_31_40: nullableCount,
23113
- pos_41_50: nullableCount,
23114
- pos_51_60: nullableCount,
23115
- pos_61_70: nullableCount,
23116
- pos_71_80: nullableCount,
23117
- pos_81_90: nullableCount,
23118
- pos_91_100: nullableCount,
23198
+ pos_1: nullableCount$1,
23199
+ pos_2_3: nullableCount$1,
23200
+ pos_4_10: nullableCount$1,
23201
+ pos_11_20: nullableCount$1,
23202
+ pos_21_30: nullableCount$1,
23203
+ pos_31_40: nullableCount$1,
23204
+ pos_41_50: nullableCount$1,
23205
+ pos_51_60: nullableCount$1,
23206
+ pos_61_70: nullableCount$1,
23207
+ pos_71_80: nullableCount$1,
23208
+ pos_81_90: nullableCount$1,
23209
+ pos_91_100: nullableCount$1,
23119
23210
  etv: nullableNonnegativeNumber,
23120
- count: nullableCount,
23211
+ count: nullableCount$1,
23121
23212
  estimated_paid_traffic_cost: nullableNonnegativeNumber,
23122
- is_new: nullableCount,
23123
- is_up: nullableCount,
23124
- is_down: nullableCount,
23125
- is_lost: nullableCount
23213
+ is_new: nullableCount$1,
23214
+ is_up: nullableCount$1,
23215
+ is_down: nullableCount$1,
23216
+ is_lost: nullableCount$1
23126
23217
  }).passthrough();
23127
23218
  const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
23128
- const paidResponseSchema = z.object({
23219
+ const paidResponseSchema$1 = z.object({
23129
23220
  status_code: z.number().int(),
23130
23221
  status_message: z.string().max(1e3),
23131
23222
  cost: z.number().finite().nonnegative().optional(),
23132
23223
  tasks_count: z.number().int().nonnegative(),
23133
23224
  tasks_error: z.number().int().nonnegative()
23134
23225
  }).passthrough();
23135
- const taskFields = {
23226
+ const taskFields$1 = {
23136
23227
  id: z.string().trim().min(1).max(100).optional(),
23137
23228
  status_code: z.number().int(),
23138
23229
  status_message: z.string().max(1e3),
@@ -23152,8 +23243,8 @@ const overviewResultSchema = z.object({
23152
23243
  items_count: z.number().int().nonnegative().optional(),
23153
23244
  items: z.array(overviewItemSchema).max(1e3).nullable().optional()
23154
23245
  }).passthrough();
23155
- const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23156
- ...taskFields,
23246
+ const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23247
+ ...taskFields$1,
23157
23248
  result: z.array(overviewResultSchema).max(10).nullable().optional()
23158
23249
  }).passthrough()).max(10) });
23159
23250
  const rankedSerpItemSchema = z.object({
@@ -23173,8 +23264,8 @@ const rankedKeywordsResultSchema = z.object({
23173
23264
  items_count: z.number().int().nonnegative().optional(),
23174
23265
  items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
23175
23266
  }).passthrough();
23176
- const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23177
- ...taskFields,
23267
+ const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23268
+ ...taskFields$1,
23178
23269
  result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
23179
23270
  }).passthrough()).max(10) });
23180
23271
  const rankingPageItemSchema = z.object({
@@ -23187,8 +23278,8 @@ const rankingPagesResultSchema = z.object({
23187
23278
  items_count: z.number().int().nonnegative().optional(),
23188
23279
  items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
23189
23280
  }).passthrough();
23190
- const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23191
- ...taskFields,
23281
+ const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23282
+ ...taskFields$1,
23192
23283
  result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
23193
23284
  }).passthrough()).max(10) });
23194
23285
  const competitorItemSchema = z.object({
@@ -23196,9 +23287,9 @@ const competitorItemSchema = z.object({
23196
23287
  avg_position: nullableNumber,
23197
23288
  median_position: nullableNumber,
23198
23289
  etv: nullableNonnegativeNumber,
23199
- keywords_count: nullableCount,
23290
+ keywords_count: nullableCount$1,
23200
23291
  visibility: nullableNonnegativeNumber,
23201
- relevant_serp_items: nullableCount,
23292
+ relevant_serp_items: nullableCount$1,
23202
23293
  keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
23203
23294
  }).passthrough();
23204
23295
  const competitorsResultSchema = z.object({
@@ -23207,8 +23298,8 @@ const competitorsResultSchema = z.object({
23207
23298
  items_count: z.number().int().nonnegative().optional(),
23208
23299
  items: z.array(competitorItemSchema).max(1e3).nullable().optional()
23209
23300
  }).passthrough();
23210
- const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23211
- ...taskFields,
23301
+ const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23302
+ ...taskFields$1,
23212
23303
  result: z.array(competitorsResultSchema).max(10).nullable().optional()
23213
23304
  }).passthrough()).max(10) });
23214
23305
  const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
@@ -23398,6 +23489,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
23398
23489
  };
23399
23490
  }
23400
23491
  //#endregion
23492
+ //#region packages/core/src/providers/dataforseo/link-schema.ts
23493
+ const nullableCount = z.number().int().nonnegative().nullable().optional();
23494
+ const nullableScore = z.number().finite().nonnegative().nullable().optional();
23495
+ const nullableText = z.string().max(2e4).nullable().optional();
23496
+ const paidResponseSchema = z.object({
23497
+ status_code: z.number().int(),
23498
+ status_message: z.string().max(1e3),
23499
+ cost: z.number().finite().nonnegative().optional(),
23500
+ tasks_count: z.number().int().nonnegative(),
23501
+ tasks_error: z.number().int().nonnegative()
23502
+ }).passthrough();
23503
+ const taskFields = {
23504
+ id: z.string().trim().min(1).max(100).optional(),
23505
+ status_code: z.number().int(),
23506
+ status_message: z.string().max(1e3),
23507
+ cost: z.number().finite().nonnegative().optional(),
23508
+ result_count: z.number().int().nonnegative().optional()
23509
+ };
23510
+ const dataForSeoLinkSummaryItemSchema = z.object({
23511
+ target: z.string().trim().min(1).max(2048).optional(),
23512
+ rank: nullableScore,
23513
+ backlinks: nullableCount,
23514
+ referring_pages: nullableCount,
23515
+ referring_domains: nullableCount,
23516
+ broken_backlinks: nullableCount,
23517
+ broken_pages: nullableCount,
23518
+ backlinks_spam_score: nullableScore,
23519
+ info: z.object({ target_spam_score: nullableScore }).passthrough().nullable().optional()
23520
+ }).passthrough();
23521
+ const dataForSeoLinkSummaryResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23522
+ ...taskFields,
23523
+ result: z.array(dataForSeoLinkSummaryItemSchema).max(10).nullable().optional()
23524
+ }).passthrough()).max(10) });
23525
+ const dataForSeoBacklinkItemSchema = z.object({
23526
+ domain_from: nullableText,
23527
+ url_from: nullableText,
23528
+ url_to: nullableText,
23529
+ anchor: nullableText,
23530
+ item_type: z.string().max(100).nullable().optional(),
23531
+ dofollow: z.boolean().nullable().optional(),
23532
+ rank: nullableScore,
23533
+ domain_from_rank: nullableScore,
23534
+ page_from_rank: nullableScore,
23535
+ backlink_spam_score: nullableScore,
23536
+ backlinks_spam_score: nullableScore,
23537
+ first_seen: z.string().max(100).nullable().optional(),
23538
+ last_visited: z.string().max(100).nullable().optional(),
23539
+ lost_date: z.string().max(100).nullable().optional(),
23540
+ is_lost: z.boolean().nullable().optional(),
23541
+ is_indirect_link: z.boolean().nullable().optional(),
23542
+ links_count: nullableCount,
23543
+ group_count: nullableCount,
23544
+ rel_attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional(),
23545
+ attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional()
23546
+ }).passthrough();
23547
+ const backlinkResultSchema = z.object({
23548
+ target: z.string().trim().min(1).max(2048).optional(),
23549
+ total_count: nullableCount,
23550
+ items_count: nullableCount,
23551
+ search_after_token: z.string().max(2e4).nullable().optional(),
23552
+ items: z.array(dataForSeoBacklinkItemSchema).max(1e3).nullable().optional()
23553
+ }).passthrough();
23554
+ const dataForSeoBacklinksResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23555
+ ...taskFields,
23556
+ result: z.array(backlinkResultSchema).max(10).nullable().optional()
23557
+ }).passthrough()).max(10) });
23558
+ const dataForSeoReferringDomainItemSchema = z.object({
23559
+ domain: z.string().trim().min(1).max(2048).nullable().optional(),
23560
+ backlinks: nullableCount,
23561
+ referring_pages: nullableCount,
23562
+ broken_backlinks: nullableCount,
23563
+ broken_pages: nullableCount,
23564
+ rank: nullableScore,
23565
+ backlinks_spam_score: nullableScore,
23566
+ first_seen: z.string().max(100).nullable().optional()
23567
+ }).passthrough();
23568
+ const referringDomainsResultSchema = z.object({
23569
+ target: z.string().trim().min(1).max(2048).optional(),
23570
+ total_count: nullableCount,
23571
+ items_count: nullableCount,
23572
+ items: z.array(dataForSeoReferringDomainItemSchema).max(1e3).nullable().optional()
23573
+ }).passthrough();
23574
+ const dataForSeoReferringDomainsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23575
+ ...taskFields,
23576
+ result: z.array(referringDomainsResultSchema).max(10).nullable().optional()
23577
+ }).passthrough()).max(10) });
23578
+ const MAX_LINK_ROWS$1 = 1e3;
23579
+ const MAX_LINK_OFFSET = 2e4;
23580
+ const LINK_ENDPOINTS = {
23581
+ summary: "v3/backlinks/summary/live",
23582
+ backlinks: "v3/backlinks/backlinks/live",
23583
+ referringDomains: "v3/backlinks/referring_domains/live"
23584
+ };
23585
+ function resultRows(response) {
23586
+ return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
23587
+ }
23588
+ function summaryRows(response) {
23589
+ return response.tasks.reduce((total, task) => total + (task.result?.length ?? 0), 0);
23590
+ }
23591
+ function commonRequest(input) {
23592
+ return {
23593
+ target: input.target,
23594
+ include_subdomains: input.scope === "domain" ? input.includeSubdomains : false,
23595
+ include_indirect_links: true,
23596
+ exclude_internal_backlinks: true,
23597
+ backlinks_status_type: "live",
23598
+ rank_scale: "one_hundred"
23599
+ };
23600
+ }
23601
+ function validatePage(input) {
23602
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1e3) throw new ProviderError({
23603
+ provider: "dataforseo",
23604
+ operation: input.operation,
23605
+ code: "configuration",
23606
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
23607
+ });
23608
+ if (!Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset > 2e4) throw new ProviderError({
23609
+ provider: "dataforseo",
23610
+ operation: input.operation,
23611
+ code: "configuration",
23612
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
23613
+ });
23614
+ }
23615
+ function linkSummaryPaidRequest(input, ttlMs) {
23616
+ return {
23617
+ operation: "link-summary",
23618
+ capability: "link-summary",
23619
+ endpoint: LINK_ENDPOINTS.summary,
23620
+ request: commonRequest(input),
23621
+ schema: dataForSeoLinkSummaryResponseSchema,
23622
+ requestedRows: 1,
23623
+ price: (account) => account.linkPrices.summary,
23624
+ context: input.context,
23625
+ ttlMs,
23626
+ refresh: input.refresh,
23627
+ rowCount: summaryRows
23628
+ };
23629
+ }
23630
+ function backlinksPaidRequest(input, ttlMs) {
23631
+ validatePage({
23632
+ limit: input.limit,
23633
+ offset: input.offset,
23634
+ operation: "backlinks"
23635
+ });
23636
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
23637
+ provider: "dataforseo",
23638
+ operation: "backlinks",
23639
+ code: "configuration",
23640
+ message: "Backlink rows need from 1 to 3 sort rules."
23641
+ });
23642
+ return {
23643
+ operation: "backlinks",
23644
+ capability: "backlinks",
23645
+ endpoint: LINK_ENDPOINTS.backlinks,
23646
+ request: {
23647
+ ...commonRequest(input),
23648
+ mode: input.mode,
23649
+ backlinks_status_type: input.status,
23650
+ limit: input.limit,
23651
+ offset: input.offset,
23652
+ order_by: input.orderBy
23653
+ },
23654
+ schema: dataForSeoBacklinksResponseSchema,
23655
+ requestedRows: input.limit,
23656
+ price: (account) => account.linkPrices.backlinks,
23657
+ context: input.context,
23658
+ ttlMs,
23659
+ refresh: input.refresh,
23660
+ rowCount: resultRows
23661
+ };
23662
+ }
23663
+ function referringDomainsPaidRequest(input, ttlMs) {
23664
+ validatePage({
23665
+ limit: input.limit,
23666
+ offset: input.offset,
23667
+ operation: "referring-domains"
23668
+ });
23669
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
23670
+ provider: "dataforseo",
23671
+ operation: "referring-domains",
23672
+ code: "configuration",
23673
+ message: "Referring-domain rows need from 1 to 3 sort rules."
23674
+ });
23675
+ return {
23676
+ operation: "referring-domains",
23677
+ capability: "referring-domains",
23678
+ endpoint: LINK_ENDPOINTS.referringDomains,
23679
+ request: {
23680
+ ...commonRequest(input),
23681
+ limit: input.limit,
23682
+ offset: input.offset,
23683
+ order_by: input.orderBy
23684
+ },
23685
+ schema: dataForSeoReferringDomainsResponseSchema,
23686
+ requestedRows: input.limit,
23687
+ price: (account) => account.linkPrices.referringDomains,
23688
+ context: input.context,
23689
+ ttlMs,
23690
+ refresh: input.refresh,
23691
+ rowCount: resultRows
23692
+ };
23693
+ }
23694
+ //#endregion
23401
23695
  //#region packages/core/src/providers/dataforseo/paid-request.ts
23402
23696
  function usdToMicros(value) {
23403
23697
  if (value === void 0) return null;
@@ -23921,41 +24215,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
23921
24215
  const MAX_KEYWORD_WORDS = 10;
23922
24216
  const MAX_DISCOVERY_SEEDS$1 = 5;
23923
24217
  const MAX_DISCOVERY_ROWS$1 = 100;
23924
- function unitPrice(components) {
23925
- const prices = components ?? [];
23926
- const total = (type) => {
23927
- const matching = prices.filter((item) => item.cost_type === type);
23928
- return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
23929
- };
23930
- return {
23931
- perRequestMicros: total("per_request"),
23932
- perResultMicros: total("per_result")
23933
- };
23934
- }
23935
- function keywordOverviewPrice(account) {
23936
- return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
23937
- }
23938
- function keywordDiscoveryPrices(account) {
23939
- return {
23940
- ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
23941
- related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
23942
- suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
23943
- };
23944
- }
23945
- function domainResearchPrices(account) {
23946
- return {
23947
- domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
23948
- rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
23949
- rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
23950
- serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
23951
- };
23952
- }
23953
- function serpLiveAdvancedPrice(account) {
23954
- return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
23955
- }
23956
- function serpTaskPostPrice(account) {
23957
- return unitPrice(account.price?.serp?.task_post?.priority_normal);
23958
- }
23959
24218
  function responseTaskIds(response) {
23960
24219
  return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
23961
24220
  }
@@ -23975,6 +24234,7 @@ var DataForSeoClient = class {
23975
24234
  serpTtlMs;
23976
24235
  accountPricingTtlMs;
23977
24236
  domainResearchTtlMs;
24237
+ linkTtlMs;
23978
24238
  spendLimits;
23979
24239
  accountPricing;
23980
24240
  accountPricingRequest;
@@ -23991,6 +24251,7 @@ var DataForSeoClient = class {
23991
24251
  this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
23992
24252
  this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
23993
24253
  this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
24254
+ this.linkTtlMs = options.linkTtlMs ?? 864e5;
23994
24255
  this.spendLimits = options.spendLimits;
23995
24256
  }
23996
24257
  async getCredentials(operation) {
@@ -24073,11 +24334,7 @@ var DataForSeoClient = class {
24073
24334
  accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
24074
24335
  accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
24075
24336
  accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
24076
- keywordOverviewPrice: keywordOverviewPrice(account),
24077
- keywordDiscoveryPrices: keywordDiscoveryPrices(account),
24078
- domainResearchPrices: domainResearchPrices(account),
24079
- serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
24080
- serpTaskPostPrice: serpTaskPostPrice(account),
24337
+ ...dataForSeoAccountPrices(account),
24081
24338
  backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
24082
24339
  aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
24083
24340
  apiVersion: response.version ?? null,
@@ -24140,6 +24397,15 @@ var DataForSeoClient = class {
24140
24397
  async serpCompetitors(input) {
24141
24398
  return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
24142
24399
  }
24400
+ async linkSummary(input) {
24401
+ return this.paidPost(linkSummaryPaidRequest(input, this.linkTtlMs));
24402
+ }
24403
+ async backlinks(input) {
24404
+ return this.paidPost(backlinksPaidRequest(input, this.linkTtlMs));
24405
+ }
24406
+ async referringDomains(input) {
24407
+ return this.paidPost(referringDomainsPaidRequest(input, this.linkTtlMs));
24408
+ }
24143
24409
  async keywordOverview(input) {
24144
24410
  const keywords = input.keywords.map((keyword) => keyword.trim());
24145
24411
  if (keywords.length < 1 || keywords.length > MAX_KEYWORDS_PER_OVERVIEW_REQUEST || keywords.some((keyword) => keyword.length < 1 || keyword.length > MAX_KEYWORD_CHARACTERS || keyword.split(/\s+/u).length > MAX_KEYWORD_WORDS)) throw new ProviderError({
@@ -24483,20 +24749,20 @@ function compareCodepoints$3(left, right) {
24483
24749
  function normalizedKeyword$2(keyword) {
24484
24750
  return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
24485
24751
  }
24486
- function missing$1(field) {
24752
+ function missing$2(field) {
24487
24753
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
24488
24754
  }
24489
24755
  function numericValue(values, field, valid) {
24490
24756
  const present = values.filter((value) => value !== null && value !== void 0);
24491
- if (present.length === 0) return missing$1(field);
24757
+ if (present.length === 0) return missing$2(field);
24492
24758
  const numbers = present.filter((value) => typeof value === "number" && valid(value));
24493
24759
  if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
24494
24760
  const unique = [...new Set(numbers)];
24495
24761
  return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
24496
24762
  }
24497
- function stringValue(values, field, normalize) {
24763
+ function stringValue$1(values, field, normalize) {
24498
24764
  const present = values.filter((value) => value !== null && value !== void 0);
24499
- if (present.length === 0) return missing$1(field);
24765
+ if (present.length === 0) return missing$2(field);
24500
24766
  const strings = present.flatMap((value) => {
24501
24767
  if (typeof value !== "string") return [];
24502
24768
  const normalized = normalize(value);
@@ -24508,7 +24774,7 @@ function stringValue(values, field, normalize) {
24508
24774
  }
24509
24775
  function monthlySearchesValue(rows) {
24510
24776
  const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
24511
- if (histories.length === 0) return missing$1("monthlySearches");
24777
+ if (histories.length === 0) return missing$2("monthlySearches");
24512
24778
  const byMonth = /* @__PURE__ */ new Map();
24513
24779
  let invalid = false;
24514
24780
  for (const history of histories) for (const item of history) {
@@ -24541,27 +24807,27 @@ function resultCountValues(rows) {
24541
24807
  function metricForKeyword(keyword, rows) {
24542
24808
  if (rows.length === 0) return {
24543
24809
  keyword,
24544
- monthlySearchVolume: missing$1("monthlySearchVolume"),
24545
- monthlySearches: missing$1("monthlySearches"),
24546
- searchVolumeUpdatedAt: missing$1("searchVolumeUpdatedAt"),
24547
- cpcUsd: missing$1("cpcUsd"),
24548
- paidCompetition: missing$1("paidCompetition"),
24549
- keywordDifficulty: missing$1("keywordDifficulty"),
24550
- intent: missing$1("intent"),
24551
- resultCount: missing$1("resultCount")
24810
+ monthlySearchVolume: missing$2("monthlySearchVolume"),
24811
+ monthlySearches: missing$2("monthlySearches"),
24812
+ searchVolumeUpdatedAt: missing$2("searchVolumeUpdatedAt"),
24813
+ cpcUsd: missing$2("cpcUsd"),
24814
+ paidCompetition: missing$2("paidCompetition"),
24815
+ keywordDifficulty: missing$2("keywordDifficulty"),
24816
+ intent: missing$2("intent"),
24817
+ resultCount: missing$2("resultCount")
24552
24818
  };
24553
24819
  return {
24554
24820
  keyword,
24555
24821
  monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
24556
24822
  monthlySearches: monthlySearchesValue(rows),
24557
- searchVolumeUpdatedAt: stringValue(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
24823
+ searchVolumeUpdatedAt: stringValue$1(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
24558
24824
  const timestamp = Date.parse(value);
24559
24825
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
24560
24826
  }),
24561
24827
  cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
24562
24828
  paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
24563
24829
  keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
24564
- intent: stringValue(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
24830
+ intent: stringValue$1(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
24565
24831
  resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
24566
24832
  };
24567
24833
  }
@@ -24614,21 +24880,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
24614
24880
  "featured_snippet",
24615
24881
  "local_pack"
24616
24882
  ]);
24617
- function missing(field) {
24883
+ function missing$1(field) {
24618
24884
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
24619
24885
  }
24620
- function numberValue(value, field) {
24621
- return value === null || value === void 0 ? missing(field) : observedValue(value);
24886
+ function numberValue$1(value, field) {
24887
+ return value === null || value === void 0 ? missing$1(field) : observedValue(value);
24622
24888
  }
24623
24889
  function countValue(value, field) {
24624
- return numberValue(value, field);
24890
+ return numberValue$1(value, field);
24625
24891
  }
24626
24892
  function sumBuckets(metrics, fields, label) {
24627
24893
  const values = fields.map((field) => metrics[field]);
24628
- return values.some((value) => value === null || value === void 0) ? missing(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
24894
+ return values.some((value) => value === null || value === void 0) ? missing$1(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
24629
24895
  }
24630
24896
  function rankingDistribution(metrics) {
24631
- if (!metrics) return missing("organic ranking distribution");
24897
+ if (!metrics) return missing$1("organic ranking distribution");
24632
24898
  const buckets = {
24633
24899
  first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
24634
24900
  top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
@@ -24679,9 +24945,9 @@ function rankingDistribution(metrics) {
24679
24945
  }
24680
24946
  function organicFootprint(metrics) {
24681
24947
  return {
24682
- estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
24948
+ estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
24683
24949
  rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
24684
- estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
24950
+ estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
24685
24951
  rankings: rankingDistribution(metrics),
24686
24952
  newRankings: countValue(metrics?.is_new, "new rankings"),
24687
24953
  improvedRankings: countValue(metrics?.is_up, "improved rankings"),
@@ -24736,7 +25002,7 @@ function researchMarket(input, operation) {
24736
25002
  });
24737
25003
  return parsed;
24738
25004
  }
24739
- function rowLimit(limit, offset = 0) {
25005
+ function rowLimit$1(limit, offset = 0) {
24740
25006
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
24741
25007
  provider: "dataforseo",
24742
25008
  operation: "domain-research",
@@ -24760,7 +25026,7 @@ function resultTypes(input, allowed) {
24760
25026
  });
24761
25027
  return normalized;
24762
25028
  }
24763
- function safeUrl$1(value) {
25029
+ function safeUrl$2(value) {
24764
25030
  if (!value) return null;
24765
25031
  try {
24766
25032
  const url = new URL(value);
@@ -24780,7 +25046,7 @@ function dedupeBy(rows, key) {
24780
25046
  }
24781
25047
  return [...grouped.entries()].sort(([left], [right]) => compareCodepoints$3(left, right)).map(([, matches]) => [...matches].sort((left, right) => compareCodepoints$3(JSON.stringify(left), JSON.stringify(right)))[0]);
24782
25048
  }
24783
- function observedNumber(value) {
25049
+ function observedNumber$1(value) {
24784
25050
  return value.state === "observed" ? value.value : -1;
24785
25051
  }
24786
25052
  function duplicateWarning(available, retained, label) {
@@ -24791,7 +25057,7 @@ function duplicateWarning(available, retained, label) {
24791
25057
  message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
24792
25058
  }] : [];
24793
25059
  }
24794
- function totalRows(values) {
25060
+ function totalRows$1(values) {
24795
25061
  const present = values.filter((value) => value !== null && value !== void 0);
24796
25062
  return present.length ? Math.max(...present) : null;
24797
25063
  }
@@ -24807,7 +25073,7 @@ function coverage(input) {
24807
25073
  nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
24808
25074
  };
24809
25075
  }
24810
- function requestContext(reportId, supplied) {
25076
+ function requestContext$1(reportId, supplied) {
24811
25077
  return supplied ?? {
24812
25078
  reportId,
24813
25079
  reportRunId: randomUUID()
@@ -24856,7 +25122,7 @@ async function dataForSeoDomainOverview(client, input) {
24856
25122
  languageCode: market.languageCode.split("-")[0],
24857
25123
  ...location,
24858
25124
  refresh: input.refresh,
24859
- context: requestContext("domain-overview", input.context)
25125
+ context: requestContext$1("domain-overview", input.context)
24860
25126
  });
24861
25127
  const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
24862
25128
  const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
@@ -24948,7 +25214,7 @@ async function dataForSeoRankedKeywords(client, input) {
24948
25214
  const market = researchMarket(input.market, "ranked-keywords");
24949
25215
  const target = rankedTarget(input.target);
24950
25216
  const offset = input.offset ?? 0;
24951
- rowLimit(input.limit, offset);
25217
+ rowLimit$1(input.limit, offset);
24952
25218
  const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
24953
25219
  const excluded = excludedTerms(input);
24954
25220
  const requestedFilters = filters$1(input, excluded);
@@ -24968,7 +25234,7 @@ async function dataForSeoRankedKeywords(client, input) {
24968
25234
  limit: input.limit,
24969
25235
  offset,
24970
25236
  refresh: input.refresh,
24971
- context: requestContext("ranked-keywords", input.context)
25237
+ context: requestContext$1("ranked-keywords", input.context)
24972
25238
  });
24973
25239
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
24974
25240
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -24977,7 +25243,7 @@ async function dataForSeoRankedKeywords(client, input) {
24977
25243
  const keywordData = row.keyword_data;
24978
25244
  const serp = row.ranked_serp_element?.serp_item;
24979
25245
  const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
24980
- const url = safeUrl$1(serp?.url);
25246
+ const url = safeUrl$2(serp?.url);
24981
25247
  if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
24982
25248
  invalidRows += 1;
24983
25249
  return [];
@@ -24988,11 +25254,11 @@ async function dataForSeoRankedKeywords(client, input) {
24988
25254
  rankGroup: serp.rank_group,
24989
25255
  rankAbsolute: serp.rank_absolute,
24990
25256
  resultType: serp.type,
24991
- estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
25257
+ estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
24992
25258
  }];
24993
25259
  });
24994
- const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber(right.monthlySearchVolume) - observedNumber(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
24995
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25260
+ const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber$1(right.monthlySearchVolume) - observedNumber$1(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
25261
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
24996
25262
  return domainEvidence({
24997
25263
  capability: "ranked-keywords",
24998
25264
  data: {
@@ -25065,7 +25331,7 @@ async function dataForSeoRankingPages(client, input) {
25065
25331
  const market = researchMarket(input.market, "ranking-pages");
25066
25332
  const target = domain(input.domain);
25067
25333
  const offset = input.offset ?? 0;
25068
- rowLimit(input.limit, offset);
25334
+ rowLimit$1(input.limit, offset);
25069
25335
  const requestedFilters = filters(input);
25070
25336
  const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
25071
25337
  const snapshot = await client.rankingPages({
@@ -25077,13 +25343,13 @@ async function dataForSeoRankingPages(client, input) {
25077
25343
  limit: input.limit,
25078
25344
  offset,
25079
25345
  refresh: input.refresh,
25080
- context: requestContext("ranking-pages", input.context)
25346
+ context: requestContext$1("ranking-pages", input.context)
25081
25347
  });
25082
25348
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
25083
25349
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
25084
25350
  let invalidRows = 0;
25085
25351
  const mappedRows = rawRows.flatMap((row) => {
25086
- const url = safeUrl$1(row.page_address);
25352
+ const url = safeUrl$2(row.page_address);
25087
25353
  if (!url) {
25088
25354
  invalidRows += 1;
25089
25355
  return [];
@@ -25093,8 +25359,8 @@ async function dataForSeoRankingPages(client, input) {
25093
25359
  organic: organicFootprint(row.metrics?.organic)
25094
25360
  }];
25095
25361
  });
25096
- const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
25097
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25362
+ const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber$1(right.organic.estimatedMonthlyTraffic) - observedNumber$1(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
25363
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
25098
25364
  return domainEvidence({
25099
25365
  capability: "relevant-pages",
25100
25366
  data: {
@@ -25139,7 +25405,7 @@ function keywords(input) {
25139
25405
  async function dataForSeoSerpCompetitors(client, input) {
25140
25406
  const market = researchMarket(input.market, "serp-competitors");
25141
25407
  const offset = input.offset ?? 0;
25142
- rowLimit(input.limit, offset);
25408
+ rowLimit$1(input.limit, offset);
25143
25409
  const requestedKeywords = keywords(input.keywords);
25144
25410
  const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
25145
25411
  const orderBy = [
@@ -25157,7 +25423,7 @@ async function dataForSeoSerpCompetitors(client, input) {
25157
25423
  limit: input.limit,
25158
25424
  offset,
25159
25425
  refresh: input.refresh,
25160
- context: requestContext("serp-competitors", input.context)
25426
+ context: requestContext$1("serp-competitors", input.context)
25161
25427
  });
25162
25428
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
25163
25429
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -25180,16 +25446,16 @@ async function dataForSeoSerpCompetitors(client, input) {
25180
25446
  return [{
25181
25447
  domain: normalizedDomain,
25182
25448
  matchedKeywords: row.keywords_count,
25183
- averagePosition: numberValue(row.avg_position, "average position"),
25184
- medianPosition: numberValue(row.median_position, "median position"),
25185
- visibility: numberValue(row.visibility, "visibility"),
25186
- estimatedMonthlyTraffic: numberValue(row.etv, "estimated monthly traffic"),
25449
+ averagePosition: numberValue$1(row.avg_position, "average position"),
25450
+ medianPosition: numberValue$1(row.median_position, "median position"),
25451
+ visibility: numberValue$1(row.visibility, "visibility"),
25452
+ estimatedMonthlyTraffic: numberValue$1(row.etv, "estimated monthly traffic"),
25187
25453
  relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
25188
25454
  keywordPositions
25189
25455
  }];
25190
25456
  });
25191
- const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber(right.visibility) - observedNumber(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
25192
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25457
+ const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber$1(right.visibility) - observedNumber$1(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
25458
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
25193
25459
  return domainEvidence({
25194
25460
  capability: "serp-competitors",
25195
25461
  data: {
@@ -26104,7 +26370,7 @@ function analyzeCompetitorGap(acquisition) {
26104
26370
  }
26105
26371
  //#endregion
26106
26372
  //#region packages/core/src/analyze/domain-research/gap-report.ts
26107
- const MAX_FINDINGS = 10;
26373
+ const MAX_FINDINGS$2 = 10;
26108
26374
  const MAX_SOURCE_BRIEFS = 5;
26109
26375
  function count(candidates, classification) {
26110
26376
  return candidates.filter((item) => item.classification === classification).length;
@@ -26119,7 +26385,7 @@ function dataStatus(input) {
26119
26385
  function findings$2(input) {
26120
26386
  const result = [];
26121
26387
  for (const [index, candidate] of input.candidates.entries()) {
26122
- if (result.length >= MAX_FINDINGS) break;
26388
+ if (result.length >= MAX_FINDINGS$2) break;
26123
26389
  if (candidate.classification === "relevant-gap-candidate") result.push({
26124
26390
  code: "relevant-competitor-gap",
26125
26391
  evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
@@ -26134,7 +26400,7 @@ function findings$2(input) {
26134
26400
  });
26135
26401
  }
26136
26402
  for (const [index, pattern] of input.patterns.entries()) {
26137
- if (result.length >= MAX_FINDINGS) break;
26403
+ if (result.length >= MAX_FINDINGS$2) break;
26138
26404
  result.push({
26139
26405
  code: "competitor-template-pattern",
26140
26406
  evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
@@ -27181,7 +27447,7 @@ function measurementWindow(input) {
27181
27447
  }
27182
27448
  //#endregion
27183
27449
  //#region packages/core/src/analyze/experiments/measure-change.ts
27184
- const defaultDependencies$5 = {
27450
+ const defaultDependencies$6 = {
27185
27451
  searchAnalytics: querySearchAnalytics,
27186
27452
  googleAnalyticsReport: runGa4Report,
27187
27453
  contentGroup: getContentGroup,
@@ -27199,7 +27465,7 @@ function counterfactualDelta(input) {
27199
27465
  const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
27200
27466
  return fixed(input.treatmentAfter - expectedAfter);
27201
27467
  }
27202
- async function measureChange(input, dependencies = defaultDependencies$5) {
27468
+ async function measureChange(input, dependencies = defaultDependencies$6) {
27203
27469
  const now = dependencies.now();
27204
27470
  const change = (input.id ? getChange(input.id) : void 0) ?? {
27205
27471
  id: "adhoc",
@@ -27924,14 +28190,14 @@ async function verifyInternalLinkCandidate(input) {
27924
28190
  }
27925
28191
  //#endregion
27926
28192
  //#region packages/core/src/analyze/internal-links.ts
27927
- const MAX_GSC_ROWS$1 = 1e5;
27928
- const defaultDependencies$4 = {
28193
+ const MAX_GSC_ROWS$2 = 1e5;
28194
+ const defaultDependencies$5 = {
27929
28195
  searchAnalytics: querySearchAnalytics,
27930
28196
  fetch: fetchPage,
27931
28197
  extract: extractPage,
27932
28198
  now: () => /* @__PURE__ */ new Date()
27933
28199
  };
27934
- async function internalLinksReport(input, dependencies = defaultDependencies$4) {
28200
+ async function internalLinksReport(input, dependencies = defaultDependencies$5) {
27935
28201
  const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
27936
28202
  targetPage.hash = "";
27937
28203
  const targetUrl = targetPage.toString();
@@ -27980,7 +28246,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
27980
28246
  dimensions: ["query", "page"],
27981
28247
  type: "web",
27982
28248
  dataState: "final",
27983
- maxRows: MAX_GSC_ROWS$1,
28249
+ maxRows: MAX_GSC_ROWS$2,
27984
28250
  dimensionFilterGroups: [{
27985
28251
  groupType: "and",
27986
28252
  filters: [{
@@ -28012,7 +28278,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28012
28278
  dimensions: ["query", "page"],
28013
28279
  type: "web",
28014
28280
  dataState: "final",
28015
- maxRows: MAX_GSC_ROWS$1
28281
+ maxRows: MAX_GSC_ROWS$2
28016
28282
  }, { refresh: input.refresh }) : {
28017
28283
  rows: [],
28018
28284
  calls: 0,
@@ -28057,8 +28323,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28057
28323
  else checkedSources += 1;
28058
28324
  }
28059
28325
  const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
28060
- const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$1);
28061
- const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$1;
28326
+ const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$2);
28327
+ const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$2;
28062
28328
  const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
28063
28329
  const structuredWarnings = uniqueInternalLinksWarnings(warnings);
28064
28330
  const dataStatus = target.verification === "technical-issue" ? "target-technical-issue" : target.verification === "failed" ? "partial" : targetRows.length === 0 ? "empty" : targetTruncated || sourceTruncated || invalidRows > 0 ? "partial" : queryCandidates && sourceResult.rows.length === 0 ? "source-empty" : analysis.selection.targetEligibleQueries === 0 || analysis.candidates.length === 0 ? "filtered" : failedChecks > 0 || uncheckedCandidates > 0 ? "partial" : "complete";
@@ -28099,18 +28365,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28099
28365
  pageFilter: alias,
28100
28366
  rowsFetched: result.rowsFetched,
28101
28367
  calls: result.calls,
28102
- possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$1
28368
+ possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
28103
28369
  })),
28104
28370
  rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
28105
28371
  calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
28106
- maxRowsPerRequest: MAX_GSC_ROWS$1,
28372
+ maxRowsPerRequest: MAX_GSC_ROWS$2,
28107
28373
  possiblyTruncated: targetTruncated
28108
28374
  },
28109
28375
  candidates: {
28110
28376
  queried: queryCandidates,
28111
28377
  rowsFetched: sourceResult.rowsFetched,
28112
28378
  calls: sourceResult.calls,
28113
- maxRows: MAX_GSC_ROWS$1,
28379
+ maxRows: MAX_GSC_ROWS$2,
28114
28380
  possiblyTruncated: sourceTruncated
28115
28381
  },
28116
28382
  completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
@@ -28129,7 +28395,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28129
28395
  minImpressions,
28130
28396
  resultLimit: limit,
28131
28397
  checkLimit,
28132
- maxGscRowsPerRequest: MAX_GSC_ROWS$1
28398
+ maxGscRowsPerRequest: MAX_GSC_ROWS$2
28133
28399
  },
28134
28400
  target: {
28135
28401
  requestedUrl: target.requestedUrl,
@@ -29069,8 +29335,8 @@ function analyzeSecondPageRows(input) {
29069
29335
  }
29070
29336
  //#endregion
29071
29337
  //#region packages/core/src/analyze/keyword-opportunities.ts
29072
- const DEFAULT_DAYS = 28;
29073
- const MAX_DAYS = 548;
29338
+ const DEFAULT_DAYS$1 = 28;
29339
+ const MAX_DAYS$1 = 548;
29074
29340
  const DEFAULT_REPORT_LIMIT = 10;
29075
29341
  const MAX_REPORT_LIMIT = 25;
29076
29342
  const DEFAULT_KEYWORD_LIMIT = 30;
@@ -29117,9 +29383,9 @@ function validateInput$2(input) {
29117
29383
  site,
29118
29384
  days: integerOption$1({
29119
29385
  value: input.days,
29120
- fallback: DEFAULT_DAYS,
29386
+ fallback: DEFAULT_DAYS$1,
29121
29387
  minimum: 1,
29122
- maximum: MAX_DAYS,
29388
+ maximum: MAX_DAYS$1,
29123
29389
  label: "Days"
29124
29390
  }),
29125
29391
  limit: integerOption$1({
@@ -30657,7 +30923,7 @@ function analyzeIndexCoverageSignals(input) {
30657
30923
  const MAX_SITEMAP_DOCUMENTS = 20;
30658
30924
  const MAX_SEARCH_ROWS = 25e4;
30659
30925
  const MAX_SITEMAP_URLS = 25e4;
30660
- const defaultDependencies$3 = {
30926
+ const defaultDependencies$4 = {
30661
30927
  loadCrawl: loadCrawlReport,
30662
30928
  latestCrawl: latestCrawlReport,
30663
30929
  searchAnalytics: querySearchAnalytics,
@@ -30689,7 +30955,7 @@ function sitemapCompleteness(input) {
30689
30955
  if (input.inventoryTruncated) return "truncated";
30690
30956
  return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
30691
30957
  }
30692
- async function indexCoverageSignals(input, dependencies = defaultDependencies$3) {
30958
+ async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
30693
30959
  const days = integerOption$2({
30694
30960
  value: input.days,
30695
30961
  fallback: 90,
@@ -31173,7 +31439,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
31173
31439
  WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
31174
31440
  )`).run(rootSite, url, rootSite, url, 20).changes;
31175
31441
  }
31176
- const defaultDependencies$2 = {
31442
+ const defaultDependencies$3 = {
31177
31443
  inspectUrl,
31178
31444
  now: () => /* @__PURE__ */ new Date(),
31179
31445
  store: {
@@ -31218,7 +31484,7 @@ function isFatalBatchError(error) {
31218
31484
  "AUTH_REQUIRED"
31219
31485
  ].includes(error.code);
31220
31486
  }
31221
- async function indexWatch(input, dependencies = defaultDependencies$2) {
31487
+ async function indexWatch(input, dependencies = defaultDependencies$3) {
31222
31488
  const languageCode = validateLanguageCode(input.languageCode);
31223
31489
  const dailyLimit = integerOption$2({
31224
31490
  value: input.dailyLimit,
@@ -33400,9 +33666,9 @@ function pseoSampleUrls(report) {
33400
33666
  }
33401
33667
  //#endregion
33402
33668
  //#region packages/core/src/analyze/pseo/audit.ts
33403
- const MAX_GSC_ROWS = 5e4;
33669
+ const MAX_GSC_ROWS$1 = 5e4;
33404
33670
  const DEFAULT_SITEMAP_URLS = 5e4;
33405
- const defaultDependencies$1 = {
33671
+ const defaultDependencies$2 = {
33406
33672
  searchAnalytics: querySearchAnalytics,
33407
33673
  fetchPage,
33408
33674
  inspectUrl,
@@ -33604,7 +33870,7 @@ async function inspectSample(input) {
33604
33870
  };
33605
33871
  }
33606
33872
  }
33607
- async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$1) {
33873
+ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
33608
33874
  const options = pseoAuditOptions(input);
33609
33875
  const now = dependencies.now();
33610
33876
  const generatedAt = now.toISOString();
@@ -33623,7 +33889,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
33623
33889
  type: "web",
33624
33890
  dataState: "final",
33625
33891
  aggregationType: "auto",
33626
- maxRows: MAX_GSC_ROWS
33892
+ maxRows: MAX_GSC_ROWS$1
33627
33893
  };
33628
33894
  input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
33629
33895
  const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
@@ -33674,7 +33940,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
33674
33940
  minimumTemplateImpressions: options.minimumTemplateImpressions,
33675
33941
  crawlSamplesPerTemplate: options.crawlSamples,
33676
33942
  inspectionSamplesPerTemplate: options.inspectSamples,
33677
- maxRowsPerRequest: MAX_GSC_ROWS,
33943
+ maxRowsPerRequest: MAX_GSC_ROWS$1,
33678
33944
  pageRowsFetched: acquisition.pageRowsFetched,
33679
33945
  queryPageRowsFetched: acquisition.queryPageRowsFetched,
33680
33946
  sitemapsRequested: input.sitemaps?.length ?? 0,
@@ -33685,7 +33951,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
33685
33951
  caveats: [`Data freshness: ${input.refresh ? "fresh fetch requested; caches bypassed where supported" : "local cache allowed; use --refresh to bypass cached provider and HTTP data"}.`, input.sitemaps?.length ? `Sitemaps: ${countLabel(input.sitemaps.length, "sitemap URL")} requested.` : "Sitemaps: none provided; template discovery used retained GSC page rows."]
33686
33952
  });
33687
33953
  }
33688
- async function pseoAuditReport(input, dependencies = defaultDependencies$1) {
33954
+ async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
33689
33955
  const acquisition = await acquirePseoAuditEvidence(input, dependencies);
33690
33956
  const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
33691
33957
  const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
@@ -34527,7 +34793,7 @@ function pseoOpportunityVerdict(input) {
34527
34793
  //#region packages/core/src/providers/dataforseo/serp-snapshot.ts
34528
34794
  const MAX_SERP_DEPTH = 100;
34529
34795
  const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
34530
- function safeUrl(value) {
34796
+ function safeUrl$1(value) {
34531
34797
  if (!value) return null;
34532
34798
  try {
34533
34799
  const parsed = new URL(value);
@@ -34540,11 +34806,11 @@ function safeUrl(value) {
34540
34806
  }
34541
34807
  }
34542
34808
  function validOrganic(item) {
34543
- return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl(item.url) !== null;
34809
+ return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl$1(item.url) !== null;
34544
34810
  }
34545
34811
  function organicResult(item) {
34546
34812
  if (!validOrganic(item)) return null;
34547
- const url = safeUrl(item.url);
34813
+ const url = safeUrl$1(item.url);
34548
34814
  if (!url) return null;
34549
34815
  return {
34550
34816
  rankGroup: item.rank_group,
@@ -34593,7 +34859,7 @@ function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
34593
34859
  field: "checkedAt",
34594
34860
  message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
34595
34861
  });
34596
- const checkUrl = safeUrl(result.check_url);
34862
+ const checkUrl = safeUrl$1(result.check_url);
34597
34863
  if (result.check_url && !checkUrl) warnings.push({
34598
34864
  code: "invalid-serp-check-url",
34599
34865
  field: "checkUrl",
@@ -37141,7 +37407,7 @@ function groupedMappings(evidence) {
37141
37407
  //#endregion
37142
37408
  //#region packages/core/src/analyze/second-page.ts
37143
37409
  const MAX_SOURCE_ROWS = 1e5;
37144
- const defaultDependencies = {
37410
+ const defaultDependencies$1 = {
37145
37411
  searchAnalytics: querySearchAnalytics,
37146
37412
  fetch: fetchPage,
37147
37413
  extract: extractPage,
@@ -37218,7 +37484,7 @@ function reportRecommendations(items) {
37218
37484
  "Treat the priority score as a triage heuristic, not an estimated traffic lift."
37219
37485
  ]);
37220
37486
  }
37221
- async function secondPage(input, dependencies = defaultDependencies) {
37487
+ async function secondPage(input, dependencies = defaultDependencies$1) {
37222
37488
  const days = integerOption({
37223
37489
  value: input.range,
37224
37490
  fallback: 28,
@@ -40384,10 +40650,10 @@ const DEFAULT_TARGET_LIMIT = 20;
40384
40650
  const MAX_TARGET_LIMIT = 50;
40385
40651
  const DEFAULT_DETAIL_PAGES = 1;
40386
40652
  const MAX_DETAIL_PAGES = 3;
40387
- const DEFAULT_ROW_LIMIT$2 = 500;
40388
- const MAX_ROW_LIMIT$2 = 1e3;
40653
+ const DEFAULT_ROW_LIMIT$3 = 500;
40654
+ const MAX_ROW_LIMIT$3 = 1e3;
40389
40655
  const MAX_CONCURRENCY = 3;
40390
- function integer(value, fallback, max, name) {
40656
+ function integer$1(value, fallback, max, name) {
40391
40657
  const resolved = value ?? fallback;
40392
40658
  if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
40393
40659
  return resolved;
@@ -40448,9 +40714,9 @@ async function targetDetails(input) {
40448
40714
  }
40449
40715
  async function collectBingLinkEvidence(input) {
40450
40716
  if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
40451
- const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
40452
- const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
40453
- const rowLimit = integer(input.rowLimit, DEFAULT_ROW_LIMIT$2, MAX_ROW_LIMIT$2, "Bing link row limit");
40717
+ const targetLimit = integer$1(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
40718
+ const detailPages = integer$1(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
40719
+ const rowLimit = integer$1(input.rowLimit, DEFAULT_ROW_LIMIT$3, MAX_ROW_LIMIT$3, "Bing link row limit");
40454
40720
  const client = input.client ?? (await createBingWebmasterClient()).client;
40455
40721
  const countRows = [];
40456
40722
  let countPagesRequested = 0;
@@ -40571,6 +40837,884 @@ async function collectBingLinkEvidence(input) {
40571
40837
  };
40572
40838
  }
40573
40839
  //#endregion
40840
+ //#region packages/core/src/links/context-sources.ts
40841
+ function urlKey(value) {
40842
+ try {
40843
+ const url = new URL(value);
40844
+ if (!["http:", "https:"].includes(url.protocol)) return null;
40845
+ url.hash = "";
40846
+ return url.toString();
40847
+ } catch {
40848
+ return null;
40849
+ }
40850
+ }
40851
+ function compareTargets(left, right) {
40852
+ return right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0);
40853
+ }
40854
+ function linkTargetCounts(evidence) {
40855
+ if (evidence.targetCounts.length) return [...evidence.targetCounts].sort(compareTargets);
40856
+ const counts = /* @__PURE__ */ new Map();
40857
+ for (const row of evidence.rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
40858
+ return [...counts].map(([targetUrl, observedLinks]) => ({
40859
+ targetUrl,
40860
+ observedLinks
40861
+ })).sort(compareTargets);
40862
+ }
40863
+ function crawlForLinkTargets(input) {
40864
+ const values = /* @__PURE__ */ new Map();
40865
+ if (!input.report) {
40866
+ for (const target of input.targets) values.set(target.targetUrl, {
40867
+ state: "unavailable",
40868
+ reason: "No matching saved crawl was available."
40869
+ });
40870
+ return {
40871
+ values,
40872
+ matched: 0
40873
+ };
40874
+ }
40875
+ const pages = /* @__PURE__ */ new Map();
40876
+ for (const page of input.report.pages) for (const value of [page.url, page.finalUrl]) {
40877
+ const key = urlKey(value);
40878
+ if (key && !pages.has(key)) pages.set(key, page);
40879
+ }
40880
+ const issues = /* @__PURE__ */ new Map();
40881
+ for (const issue of input.report.issues) {
40882
+ const key = urlKey(issue.url);
40883
+ if (!key) continue;
40884
+ const ids = issues.get(key) ?? /* @__PURE__ */ new Set();
40885
+ ids.add(issue.ruleId);
40886
+ issues.set(key, ids);
40887
+ }
40888
+ let matched = 0;
40889
+ for (const target of input.targets) {
40890
+ const key = urlKey(target.targetUrl);
40891
+ const page = key ? pages.get(key) : void 0;
40892
+ if (!page) {
40893
+ values.set(target.targetUrl, {
40894
+ state: "not-observed",
40895
+ reason: "The target URL was not present in the retained crawl pages."
40896
+ });
40897
+ continue;
40898
+ }
40899
+ matched += 1;
40900
+ values.set(target.targetUrl, {
40901
+ state: "observed",
40902
+ reportId: input.report.id,
40903
+ observedAt: input.report.generatedAt,
40904
+ status: page.status,
40905
+ finalUrl: page.finalUrl,
40906
+ indexable: page.indexable,
40907
+ canonical: page.canonical ?? null,
40908
+ issueIds: [...issues.get(key) ?? []].sort()
40909
+ });
40910
+ }
40911
+ return {
40912
+ values,
40913
+ matched
40914
+ };
40915
+ }
40916
+ function aggregateLinkSearchRows(rows) {
40917
+ const values = /* @__PURE__ */ new Map();
40918
+ for (const row of rows) {
40919
+ const page = row.keys[0];
40920
+ const key = page ? urlKey(page) : null;
40921
+ if (!key) continue;
40922
+ const current = values.get(key) ?? {
40923
+ clicks: 0,
40924
+ impressions: 0,
40925
+ positionWeight: 0
40926
+ };
40927
+ current.clicks += row.clicks;
40928
+ current.impressions += row.impressions;
40929
+ current.positionWeight += row.position * row.impressions;
40930
+ values.set(key, current);
40931
+ }
40932
+ return values;
40933
+ }
40934
+ //#endregion
40935
+ //#region packages/core/src/links/context-findings.ts
40936
+ const MAX_FINDINGS$1 = 50;
40937
+ function findingPriority(input) {
40938
+ const searchValue = input.search.state === "observed" && (input.search.clicks > 0 || input.search.impressions >= 100);
40939
+ if (input.code === "linked-broken-target" || searchValue) return "high";
40940
+ if (input.observedLinks >= 5) return "medium";
40941
+ return "low";
40942
+ }
40943
+ function finding(input) {
40944
+ const crawl = input.row.crawl;
40945
+ if (crawl.state !== "observed") throw new Error("A link finding requires observed crawl evidence.");
40946
+ const searchEvidence = input.row.searchConsole.state === "observed" ? ` Search Console retained ${input.row.searchConsole.clicks.toFixed(0)} clicks and ${input.row.searchConsole.impressions.toFixed(0)} impressions for this page in the selected window.` : "";
40947
+ const common = {
40948
+ priority: findingPriority({
40949
+ code: input.code,
40950
+ observedLinks: input.row.observedLinks,
40951
+ crawl,
40952
+ search: input.row.searchConsole
40953
+ }),
40954
+ heuristic: true,
40955
+ targetUrl: input.row.targetUrl,
40956
+ observedLinks: input.row.observedLinks,
40957
+ evidenceRefs: [input.row.targetUrl, crawl.reportId]
40958
+ };
40959
+ if (input.code === "linked-broken-target") return {
40960
+ ...common,
40961
+ code: input.code,
40962
+ principle: "Linked target pages should resolve intentionally.",
40963
+ evidence: `The saved crawl observed status ${crawl.status} for a target with ${input.row.observedLinks} retained representative links.${searchEvidence}`,
40964
+ action: "Confirm whether the page should exist. Restore it or add one direct redirect to a genuinely equivalent live page when the missing URL is unintended.",
40965
+ verify: "Run redirect-trace for the target, then recrawl it and confirm a stable intended response."
40966
+ };
40967
+ if (input.code === "linked-redirect-target") return {
40968
+ ...common,
40969
+ code: input.code,
40970
+ principle: "External links should reach a stable final destination.",
40971
+ evidence: `The saved crawl observed ${input.row.targetUrl} resolving to ${crawl.finalUrl}.${searchEvidence}`,
40972
+ action: "Check that the redirect is intentional, direct and semantically equivalent. Update links you control to the final URL and ask important external sources to update only when the change is worth the effort.",
40973
+ verify: "Run redirect-trace and confirm one stable hop to the intended indexable page."
40974
+ };
40975
+ if (input.code === "linked-canonical-conflict") return {
40976
+ ...common,
40977
+ code: input.code,
40978
+ principle: "Canonical hints should agree with the intended linked page.",
40979
+ evidence: `The saved crawl observed canonical ${crawl.canonical} on a linked target that resolved to ${crawl.finalUrl}.${searchEvidence}`,
40980
+ action: "Confirm the preferred URL. Align the canonical and redirect only if the current disagreement is unintended.",
40981
+ verify: "Audit the final page and inspect the declared and Google-selected canonical before changing it."
40982
+ };
40983
+ return {
40984
+ ...common,
40985
+ code: input.code,
40986
+ principle: "A linked page should be indexable when search visibility is intended.",
40987
+ evidence: `The saved crawl marked a linked target as non-indexable.${searchEvidence}`,
40988
+ action: "Confirm whether non-indexing is intentional. Remove the blocking signal or redirect to an equivalent indexable page only when this URL should appear in search.",
40989
+ verify: "Audit the page, then use URL Inspection when the intended state is indexable."
40990
+ };
40991
+ }
40992
+ function linkTargetFindings(rows) {
40993
+ return rows.flatMap((row) => {
40994
+ const crawl = row.crawl;
40995
+ if (crawl.state !== "observed") return [];
40996
+ if (crawl.status >= 400) return [finding({
40997
+ row,
40998
+ code: "linked-broken-target"
40999
+ })];
41000
+ if (urlKey(crawl.finalUrl) !== urlKey(row.targetUrl)) return [finding({
41001
+ row,
41002
+ code: "linked-redirect-target"
41003
+ })];
41004
+ if (crawl.canonical && urlKey(crawl.canonical) !== urlKey(crawl.finalUrl)) return [finding({
41005
+ row,
41006
+ code: "linked-canonical-conflict"
41007
+ })];
41008
+ if (!crawl.indexable) return [finding({
41009
+ row,
41010
+ code: "linked-non-indexable-target"
41011
+ })];
41012
+ return [];
41013
+ }).sort((left, right) => ({
41014
+ high: 0,
41015
+ medium: 1,
41016
+ low: 2
41017
+ })[left.priority] - {
41018
+ high: 0,
41019
+ medium: 1,
41020
+ low: 2
41021
+ }[right.priority] || right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0)).slice(0, MAX_FINDINGS$1);
41022
+ }
41023
+ //#endregion
41024
+ //#region packages/core/src/links/context.ts
41025
+ const DEFAULT_DAYS = 90;
41026
+ const MAX_DAYS = 548;
41027
+ const DEFAULT_CONTEXT_LIMIT = 100;
41028
+ const MAX_CONTEXT_LIMIT = 100;
41029
+ const MAX_GSC_ROWS = 25e3;
41030
+ const defaultDependencies = {
41031
+ latestCrawl: latestCrawlReport,
41032
+ searchAnalytics: querySearchAnalytics,
41033
+ now: () => /* @__PURE__ */ new Date()
41034
+ };
41035
+ function integer(input) {
41036
+ const value = input.value ?? input.fallback;
41037
+ if (!Number.isSafeInteger(value) || value < 1 || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between 1 and ${input.maximum}.`);
41038
+ return value;
41039
+ }
41040
+ function searchEvidence(input) {
41041
+ if (!input.site) return {
41042
+ state: "unavailable",
41043
+ reason: "No Search Console property was selected for this report."
41044
+ };
41045
+ if (input.error) return {
41046
+ state: "unavailable",
41047
+ reason: input.error
41048
+ };
41049
+ if (!input.aggregate) return {
41050
+ state: "not-retained",
41051
+ reason: input.retainedLimitReached ? `The bounded Search Console response reached ${MAX_GSC_ROWS} rows and did not retain this page.` : "Search Console did not retain a page row for this URL in the selected window."
41052
+ };
41053
+ return {
41054
+ state: "observed",
41055
+ clicks: input.aggregate.clicks,
41056
+ impressions: input.aggregate.impressions,
41057
+ ctr: input.aggregate.impressions > 0 ? input.aggregate.clicks / input.aggregate.impressions : 0,
41058
+ position: input.aggregate.impressions > 0 ? input.aggregate.positionWeight / input.aggregate.impressions : 0
41059
+ };
41060
+ }
41061
+ async function linkTargetContext(input, dependencies = defaultDependencies) {
41062
+ const days = integer({
41063
+ value: input.days,
41064
+ fallback: DEFAULT_DAYS,
41065
+ maximum: MAX_DAYS,
41066
+ label: "Link context days"
41067
+ });
41068
+ const limit = integer({
41069
+ value: input.limit,
41070
+ fallback: DEFAULT_CONTEXT_LIMIT,
41071
+ maximum: MAX_CONTEXT_LIMIT,
41072
+ label: "Link context limit"
41073
+ });
41074
+ const available = linkTargetCounts(input.evidence);
41075
+ const selected = available.slice(0, limit);
41076
+ const crawlLookup = input.crawlSite ?? input.searchConsoleSite;
41077
+ const crawl = crawlLookup ? dependencies.latestCrawl(crawlLookup) : void 0;
41078
+ const crawlValues = crawlForLinkTargets({
41079
+ report: crawl,
41080
+ targets: selected
41081
+ });
41082
+ const warnings = [];
41083
+ const range = finalGscDateRange(days, dependencies.now());
41084
+ let searchRows = /* @__PURE__ */ new Map();
41085
+ let searchCalls = 0;
41086
+ let searchRowsFetched = 0;
41087
+ let searchError = null;
41088
+ if (input.searchConsoleSite) try {
41089
+ const result = await dependencies.searchAnalytics(input.searchConsoleSite, {
41090
+ ...range,
41091
+ dimensions: ["page"],
41092
+ type: "web",
41093
+ dataState: "final",
41094
+ rowLimit: MAX_GSC_ROWS,
41095
+ maxRows: MAX_GSC_ROWS
41096
+ }, { refresh: input.refresh });
41097
+ searchRows = aggregateLinkSearchRows(result.rows);
41098
+ searchCalls = result.calls;
41099
+ searchRowsFetched = result.rowsFetched;
41100
+ } catch (error) {
41101
+ searchError = error instanceof Error ? error.message : "Search Console evidence could not be loaded.";
41102
+ warnings.push(`Search Console context was unavailable. ${searchError}`);
41103
+ }
41104
+ let searchMatches = 0;
41105
+ const retainedLimitReached = searchRowsFetched >= MAX_GSC_ROWS;
41106
+ const rows = selected.map((target) => {
41107
+ const aggregate = searchRows.get(urlKey(target.targetUrl) ?? "");
41108
+ if (aggregate) searchMatches += 1;
41109
+ return {
41110
+ ...target,
41111
+ crawl: crawlValues.values.get(target.targetUrl) ?? {
41112
+ state: "not-observed",
41113
+ reason: "The target URL was not present in the retained crawl pages."
41114
+ },
41115
+ searchConsole: searchEvidence({
41116
+ site: input.searchConsoleSite,
41117
+ error: searchError,
41118
+ retainedLimitReached,
41119
+ aggregate
41120
+ })
41121
+ };
41122
+ });
41123
+ if (!crawl) warnings.push("No matching saved crawl was available, so target response and indexability checks were skipped.");
41124
+ if (retainedLimitReached) warnings.push(`Search Console reached the ${MAX_GSC_ROWS}-row acquisition limit. Missing target rows remain not retained rather than zero.`);
41125
+ const unavailableSources = Number(!crawl) + Number(!input.searchConsoleSite);
41126
+ return {
41127
+ schemaVersion: 1,
41128
+ dataStatus: unavailableSources === 2 ? "unavailable" : unavailableSources > 0 || searchError || retainedLimitReached ? "partial" : "complete",
41129
+ selection: {
41130
+ availableTargets: available.length,
41131
+ returnedTargets: rows.length,
41132
+ omittedTargets: Math.max(0, available.length - rows.length),
41133
+ limit
41134
+ },
41135
+ provenance: {
41136
+ crawl: {
41137
+ status: crawl ? "joined" : "unavailable",
41138
+ reportId: crawl?.id ?? null,
41139
+ observedAt: crawl?.generatedAt ?? null,
41140
+ availablePages: crawl?.pages.length ?? 0,
41141
+ matchedTargets: crawlValues.matched
41142
+ },
41143
+ searchConsole: {
41144
+ status: !input.searchConsoleSite || searchError ? "unavailable" : retainedLimitReached ? "partial" : "joined",
41145
+ site: input.searchConsoleSite ?? null,
41146
+ range: input.searchConsoleSite ? {
41147
+ ...range,
41148
+ days
41149
+ } : null,
41150
+ calls: searchCalls,
41151
+ rowsFetched: searchRowsFetched,
41152
+ retainedRowLimit: MAX_GSC_ROWS,
41153
+ retainedRowLimitReached: retainedLimitReached,
41154
+ matchedTargets: searchMatches
41155
+ }
41156
+ },
41157
+ rows,
41158
+ findings: linkTargetFindings(rows),
41159
+ warnings
41160
+ };
41161
+ }
41162
+ //#endregion
41163
+ //#region packages/core/src/providers/dataforseo/link-research-shared.ts
41164
+ function linkTarget(value, requestedScope = "domain") {
41165
+ const raw = value.trim();
41166
+ if (!raw || raw.length > 2048) throw invalidTarget();
41167
+ if (requestedScope === "page") try {
41168
+ const url = new URL(raw);
41169
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
41170
+ url.hash = "";
41171
+ return {
41172
+ target: url.toString(),
41173
+ scope: "page"
41174
+ };
41175
+ } catch {
41176
+ throw invalidTarget();
41177
+ }
41178
+ try {
41179
+ const hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.toLowerCase().replace(/^www\./u, "").replace(/\.$/u, "");
41180
+ if (!hostname || hostname.length > 253 || hostname.includes("..") || !hostname.includes(".") || !/^[a-z0-9.-]+$/u.test(hostname)) throw new Error();
41181
+ return {
41182
+ target: hostname,
41183
+ scope: "domain"
41184
+ };
41185
+ } catch {
41186
+ throw invalidTarget();
41187
+ }
41188
+ }
41189
+ function invalidTarget() {
41190
+ return new ProviderError({
41191
+ provider: "dataforseo",
41192
+ operation: "link-evidence",
41193
+ code: "configuration",
41194
+ message: "Use a valid domain, or choose page scope and pass an absolute URL."
41195
+ });
41196
+ }
41197
+ function linkRowLimit(limit, offset = 0) {
41198
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
41199
+ provider: "dataforseo",
41200
+ operation: "link-evidence",
41201
+ code: "configuration",
41202
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
41203
+ });
41204
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 2e4) throw new ProviderError({
41205
+ provider: "dataforseo",
41206
+ operation: "link-evidence",
41207
+ code: "configuration",
41208
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
41209
+ });
41210
+ }
41211
+ function requestContext(reportId, supplied) {
41212
+ return supplied ?? {
41213
+ reportId,
41214
+ reportRunId: randomUUID()
41215
+ };
41216
+ }
41217
+ function missing(field) {
41218
+ return unavailableValue("missing", `DataForSEO omitted ${field}.`);
41219
+ }
41220
+ function numberValue(value, field) {
41221
+ return value === null || value === void 0 ? missing(field) : observedValue(value);
41222
+ }
41223
+ function stringValue(value, field) {
41224
+ return value ? observedValue(value) : missing(field);
41225
+ }
41226
+ function metric(id, label, value) {
41227
+ return value === null || value === void 0 ? [] : [{
41228
+ provider: "dataforseo",
41229
+ id,
41230
+ label,
41231
+ value,
41232
+ scale: {
41233
+ minimum: 0,
41234
+ maximum: 100
41235
+ }
41236
+ }];
41237
+ }
41238
+ function safeUrl(value) {
41239
+ if (!value) return null;
41240
+ try {
41241
+ const url = new URL(value);
41242
+ if (!["http:", "https:"].includes(url.protocol)) return null;
41243
+ url.username = "";
41244
+ url.password = "";
41245
+ url.hash = "";
41246
+ return url.toString();
41247
+ } catch {
41248
+ return null;
41249
+ }
41250
+ }
41251
+ function normalizedDate(value) {
41252
+ if (!value) return null;
41253
+ const parsed = Date.parse(value);
41254
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
41255
+ }
41256
+ function uniqueSorted(values = []) {
41257
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodepoints$3);
41258
+ }
41259
+ function totalRows(values) {
41260
+ const present = values.filter((value) => value !== null && value !== void 0);
41261
+ return present.length ? Math.max(...present) : null;
41262
+ }
41263
+ function linkCoverage(input) {
41264
+ const hasMore = input.providerTotalComparable !== false && input.providerTotalRows !== null ? input.offset + input.returnedRows < input.providerTotalRows : input.returnedRows >= input.requestedRows;
41265
+ return {
41266
+ requestedRows: input.requestedRows,
41267
+ returnedRows: input.returnedRows,
41268
+ retainedRows: input.retainedRows,
41269
+ invalidRows: input.invalidRows,
41270
+ providerTotalRows: input.providerTotalRows,
41271
+ completeness: input.invalidRows > 0 ? "partial" : hasMore ? "capped" : input.filtered ? "filtered" : "complete",
41272
+ nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
41273
+ };
41274
+ }
41275
+ function linkEvidence(input) {
41276
+ return {
41277
+ schemaVersion: 1,
41278
+ provider: "dataforseo",
41279
+ capability: input.capability,
41280
+ data: input.data,
41281
+ observedAt: input.snapshot.observedAt,
41282
+ market: null,
41283
+ coverage: input.coverage,
41284
+ cache: input.snapshot.cache,
41285
+ cost: input.snapshot.cost,
41286
+ request: {
41287
+ operation: input.capability,
41288
+ endpoint: input.endpoint,
41289
+ limit: input.limit,
41290
+ filters: input.filters,
41291
+ sort: input.sort
41292
+ },
41293
+ warnings: input.warnings
41294
+ };
41295
+ }
41296
+ function mappingWarnings(input) {
41297
+ return [
41298
+ ...input.snapshotWarnings,
41299
+ ...input.invalidRows ? [{
41300
+ code: `invalid-${input.label}-rows`,
41301
+ field: "data.rows",
41302
+ message: `${input.invalidRows} ${input.label} row${input.invalidRows === 1 ? "" : "s"} lacked required fields and were omitted.`
41303
+ }] : [],
41304
+ ...input.duplicateRows ? [{
41305
+ code: `duplicate-${input.label}-rows`,
41306
+ field: "data.rows",
41307
+ message: `${input.duplicateRows} duplicate ${input.label} row${input.duplicateRows === 1 ? "" : "s"} were collapsed deterministically.`
41308
+ }] : []
41309
+ ];
41310
+ }
41311
+ //#endregion
41312
+ //#region packages/core/src/providers/dataforseo/backlinks.ts
41313
+ function metricValue(row, id) {
41314
+ return row.metrics.find((item) => item.id === id)?.value ?? -1;
41315
+ }
41316
+ function compareRows(left, right) {
41317
+ return metricValue(right, "backlink-rank") - metricValue(left, "backlink-rank") || metricValue(right, "source-domain-rank") - metricValue(left, "source-domain-rank") || compareCodepoints$3(left.sourceDomain, right.sourceDomain) || compareCodepoints$3(left.sourceUrl, right.sourceUrl) || compareCodepoints$3(left.targetUrl, right.targetUrl) || compareCodepoints$3(left.anchorText ?? "", right.anchorText ?? "");
41318
+ }
41319
+ function dedupe(rows) {
41320
+ const sorted = [...rows].sort(compareRows);
41321
+ const seen = /* @__PURE__ */ new Set();
41322
+ return sorted.filter((row) => {
41323
+ const key = `${row.sourceUrl}\0${row.targetUrl}\0${row.anchorText ?? ""}`;
41324
+ if (seen.has(key)) return false;
41325
+ seen.add(key);
41326
+ return true;
41327
+ });
41328
+ }
41329
+ async function dataForSeoBacklinks(client, input) {
41330
+ const normalized = linkTarget(input.target, input.scope);
41331
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
41332
+ const offset = input.offset ?? 0;
41333
+ linkRowLimit(input.limit, offset);
41334
+ const mode = input.mode ?? "representative";
41335
+ const status = input.status ?? "live";
41336
+ const providerMode = mode === "representative" ? "one_per_domain" : "as_is";
41337
+ const orderBy = [
41338
+ "rank,desc",
41339
+ "domain_from_rank,desc",
41340
+ "url_from,asc"
41341
+ ];
41342
+ const snapshot = await client.backlinks({
41343
+ ...normalized,
41344
+ includeSubdomains,
41345
+ mode: providerMode,
41346
+ status,
41347
+ limit: input.limit,
41348
+ offset,
41349
+ orderBy,
41350
+ refresh: input.refresh,
41351
+ context: requestContext("link-evidence", input.context)
41352
+ });
41353
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
41354
+ const rawRows = results.flatMap((result) => result.items ?? []);
41355
+ let invalidRows = 0;
41356
+ const mapped = rawRows.flatMap((row) => {
41357
+ const sourceUrl = safeUrl(row.url_from);
41358
+ const targetUrl = safeUrl(row.url_to);
41359
+ if (!sourceUrl || !targetUrl) {
41360
+ invalidRows += 1;
41361
+ return [];
41362
+ }
41363
+ return [{
41364
+ sourceUrl,
41365
+ sourceDomain: new URL(sourceUrl).hostname.toLowerCase(),
41366
+ targetUrl,
41367
+ anchorText: row.anchor ?? null,
41368
+ linkType: row.item_type ?? null,
41369
+ dofollow: row.dofollow ?? null,
41370
+ attributes: uniqueSorted(row.rel_attributes ?? row.attributes ?? []),
41371
+ firstSeenAt: normalizedDate(row.first_seen),
41372
+ lastSeenAt: normalizedDate(row.lost_date ?? row.last_visited),
41373
+ state: row.is_lost || row.lost_date ? "lost" : "live",
41374
+ indirect: row.is_indirect_link ?? null,
41375
+ linksFromPage: row.links_count ?? null,
41376
+ linksFromDomain: row.group_count ?? null,
41377
+ metrics: [
41378
+ ...metric("backlink-rank", "DataForSEO backlink rank", row.rank),
41379
+ ...metric("source-domain-rank", "DataForSEO source domain rank", row.domain_from_rank),
41380
+ ...metric("source-page-rank", "DataForSEO source page rank", row.page_from_rank),
41381
+ ...metric("backlink-spam-score", "DataForSEO backlink spam score", row.backlink_spam_score ?? row.backlinks_spam_score)
41382
+ ]
41383
+ }];
41384
+ });
41385
+ const rows = dedupe(mapped);
41386
+ const duplicateRows = mapped.length - rows.length;
41387
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
41388
+ return linkEvidence({
41389
+ capability: "backlinks",
41390
+ data: {
41391
+ target: normalized.target,
41392
+ mode,
41393
+ rows,
41394
+ totalRows: providerTotalRows
41395
+ },
41396
+ snapshot,
41397
+ coverage: linkCoverage({
41398
+ requestedRows: input.limit,
41399
+ returnedRows: rawRows.length,
41400
+ retainedRows: rows.length,
41401
+ invalidRows,
41402
+ providerTotalRows,
41403
+ offset,
41404
+ filtered: mode === "representative" || status !== "all",
41405
+ providerTotalComparable: mode !== "representative"
41406
+ }),
41407
+ endpoint: LINK_ENDPOINTS.backlinks,
41408
+ limit: input.limit,
41409
+ filters: {
41410
+ scope: normalized.scope,
41411
+ includeSubdomains,
41412
+ mode,
41413
+ status,
41414
+ includeIndirectLinks: true,
41415
+ excludeInternalBacklinks: true,
41416
+ offset
41417
+ },
41418
+ sort: orderBy,
41419
+ warnings: mappingWarnings({
41420
+ snapshotWarnings: snapshot.warnings,
41421
+ invalidRows,
41422
+ duplicateRows,
41423
+ label: "backlink"
41424
+ })
41425
+ });
41426
+ }
41427
+ //#endregion
41428
+ //#region packages/core/src/providers/dataforseo/link-summary.ts
41429
+ async function dataForSeoLinkSummary(client, input) {
41430
+ const normalized = linkTarget(input.target, input.scope);
41431
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
41432
+ const snapshot = await client.linkSummary({
41433
+ ...normalized,
41434
+ includeSubdomains,
41435
+ refresh: input.refresh,
41436
+ context: requestContext("link-evidence", input.context)
41437
+ });
41438
+ const rows = snapshot.response.tasks.flatMap((task) => task.result ?? []);
41439
+ const row = [...rows].sort((left, right) => compareCodepoints$3(left.target ?? "", right.target ?? ""))[0];
41440
+ const data = {
41441
+ target: normalized.target,
41442
+ scope: normalized.scope,
41443
+ backlinks: numberValue(row?.backlinks, "total backlinks"),
41444
+ referringDomains: numberValue(row?.referring_domains, "referring domains"),
41445
+ referringPages: numberValue(row?.referring_pages, "referring pages"),
41446
+ brokenBacklinks: numberValue(row?.broken_backlinks, "broken backlinks"),
41447
+ brokenPages: numberValue(row?.broken_pages, "broken referring pages"),
41448
+ metrics: [
41449
+ ...metric("rank", "DataForSEO rank", row?.rank),
41450
+ ...metric("backlinks-spam-score", "DataForSEO backlink spam score", row?.backlinks_spam_score),
41451
+ ...metric("target-spam-score", "DataForSEO target spam score", row?.info?.target_spam_score)
41452
+ ]
41453
+ };
41454
+ const retainedRows = row ? 1 : 0;
41455
+ return linkEvidence({
41456
+ capability: "link-summary",
41457
+ data,
41458
+ snapshot,
41459
+ coverage: {
41460
+ requestedRows: 1,
41461
+ returnedRows: rows.length,
41462
+ retainedRows,
41463
+ invalidRows: 0,
41464
+ providerTotalRows: rows.length,
41465
+ completeness: row ? "complete" : "unavailable",
41466
+ nextCursor: null
41467
+ },
41468
+ endpoint: LINK_ENDPOINTS.summary,
41469
+ limit: 1,
41470
+ filters: {
41471
+ scope: normalized.scope,
41472
+ includeSubdomains,
41473
+ status: "live",
41474
+ includeIndirectLinks: true,
41475
+ excludeInternalBacklinks: true
41476
+ },
41477
+ sort: [],
41478
+ warnings: [
41479
+ ...snapshot.warnings,
41480
+ ...rows.length > 1 ? [{
41481
+ code: "multiple-link-summary-rows",
41482
+ field: "data",
41483
+ message: "DataForSEO returned more than one summary row. One row was retained deterministically."
41484
+ }] : [],
41485
+ ...!row ? [{
41486
+ code: "missing-link-summary-row",
41487
+ field: "data",
41488
+ message: "DataForSEO returned no summary row. Counts remain unavailable rather than zero."
41489
+ }] : []
41490
+ ]
41491
+ });
41492
+ }
41493
+ //#endregion
41494
+ //#region packages/core/src/providers/dataforseo/referring-domains.ts
41495
+ function observedNumber(row) {
41496
+ return row.backlinks.state === "observed" ? row.backlinks.value : -1;
41497
+ }
41498
+ async function dataForSeoReferringDomains(client, input) {
41499
+ const normalized = linkTarget(input.target, input.scope);
41500
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
41501
+ const offset = input.offset ?? 0;
41502
+ linkRowLimit(input.limit, offset);
41503
+ const orderBy = [
41504
+ "backlinks,desc",
41505
+ "rank,desc",
41506
+ "domain,asc"
41507
+ ];
41508
+ const snapshot = await client.referringDomains({
41509
+ ...normalized,
41510
+ includeSubdomains,
41511
+ limit: input.limit,
41512
+ offset,
41513
+ orderBy,
41514
+ refresh: input.refresh,
41515
+ context: requestContext("link-evidence", input.context)
41516
+ });
41517
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
41518
+ const rawRows = results.flatMap((result) => result.items ?? []);
41519
+ let invalidRows = 0;
41520
+ const mapped = rawRows.flatMap((row) => {
41521
+ const domain = row.domain?.trim().toLowerCase();
41522
+ if (!domain) {
41523
+ invalidRows += 1;
41524
+ return [];
41525
+ }
41526
+ const firstSeenAt = normalizedDate(row.first_seen);
41527
+ return [{
41528
+ domain,
41529
+ backlinks: numberValue(row.backlinks, "domain backlinks"),
41530
+ referringPages: numberValue(row.referring_pages, "domain referring pages"),
41531
+ brokenBacklinks: numberValue(row.broken_backlinks, "domain broken backlinks"),
41532
+ brokenPages: numberValue(row.broken_pages, "domain broken pages"),
41533
+ firstSeenAt: stringValue(firstSeenAt, "domain first seen date"),
41534
+ metrics: [...metric("source-domain-rank", "DataForSEO source domain rank", row.rank), ...metric("domain-backlinks-spam-score", "DataForSEO domain backlink spam score", row.backlinks_spam_score)]
41535
+ }];
41536
+ });
41537
+ const byDomain = /* @__PURE__ */ new Map();
41538
+ for (const row of mapped.sort((left, right) => observedNumber(right) - observedNumber(left) || compareCodepoints$3(left.domain, right.domain))) if (!byDomain.has(row.domain)) byDomain.set(row.domain, row);
41539
+ const rows = [...byDomain.values()];
41540
+ const duplicateRows = mapped.length - rows.length;
41541
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
41542
+ return linkEvidence({
41543
+ capability: "referring-domains",
41544
+ data: {
41545
+ target: normalized.target,
41546
+ rows,
41547
+ totalRows: providerTotalRows
41548
+ },
41549
+ snapshot,
41550
+ coverage: linkCoverage({
41551
+ requestedRows: input.limit,
41552
+ returnedRows: rawRows.length,
41553
+ retainedRows: rows.length,
41554
+ invalidRows,
41555
+ providerTotalRows,
41556
+ offset,
41557
+ filtered: true
41558
+ }),
41559
+ endpoint: LINK_ENDPOINTS.referringDomains,
41560
+ limit: input.limit,
41561
+ filters: {
41562
+ scope: normalized.scope,
41563
+ includeSubdomains,
41564
+ status: "live",
41565
+ includeIndirectLinks: true,
41566
+ excludeInternalBacklinks: true,
41567
+ offset
41568
+ },
41569
+ sort: orderBy,
41570
+ warnings: mappingWarnings({
41571
+ snapshotWarnings: snapshot.warnings,
41572
+ invalidRows,
41573
+ duplicateRows,
41574
+ label: "referring-domain"
41575
+ })
41576
+ });
41577
+ }
41578
+ //#endregion
41579
+ //#region packages/core/src/providers/dataforseo/link-research.ts
41580
+ var DataForSeoLinkProvider = class {
41581
+ provider = "dataforseo";
41582
+ capabilitySupport = [
41583
+ "link-summary",
41584
+ "referring-domains",
41585
+ "backlinks"
41586
+ ].map((capability) => ({
41587
+ capability,
41588
+ status: "available",
41589
+ markets: "all"
41590
+ }));
41591
+ client;
41592
+ constructor(options = {}) {
41593
+ this.client = options.client ?? new DataForSeoClient(options);
41594
+ }
41595
+ linkSummary(input) {
41596
+ return dataForSeoLinkSummary(this.client, input);
41597
+ }
41598
+ backlinks(input) {
41599
+ return dataForSeoBacklinks(this.client, input);
41600
+ }
41601
+ referringDomains(input) {
41602
+ return dataForSeoReferringDomains(this.client, input);
41603
+ }
41604
+ };
41605
+ //#endregion
41606
+ //#region packages/core/src/links/dataforseo.ts
41607
+ const DEFAULT_ROW_LIMIT$2 = 100;
41608
+ const MAX_ROW_LIMIT$2 = 500;
41609
+ function rowLimit(value) {
41610
+ const limit = value ?? DEFAULT_ROW_LIMIT$2;
41611
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_ROW_LIMIT$2) throw new SeoError("INVALID_INPUT", `DataForSEO link row limit must be between 1 and ${MAX_ROW_LIMIT$2}.`);
41612
+ return limit;
41613
+ }
41614
+ async function collectDataForSeoLinkEvidence(input) {
41615
+ const limit = rowLimit(input.rowLimit);
41616
+ const provider = input.provider ?? new DataForSeoLinkProvider();
41617
+ const context = input.context ?? {
41618
+ reportId: "link-evidence",
41619
+ reportRunId: randomUUID()
41620
+ };
41621
+ const summary = await provider.linkSummary({
41622
+ target: input.target,
41623
+ scope: input.scope,
41624
+ includeSubdomains: input.includeSubdomains,
41625
+ refresh: input.refresh,
41626
+ context
41627
+ });
41628
+ const backlinks = await provider.backlinks({
41629
+ target: summary.data.target,
41630
+ scope: summary.data.scope,
41631
+ includeSubdomains: input.includeSubdomains,
41632
+ mode: "representative",
41633
+ status: "live",
41634
+ limit,
41635
+ refresh: input.refresh,
41636
+ context
41637
+ });
41638
+ const rows = backlinks.data.rows.map((row) => ({
41639
+ sourceUrl: row.sourceUrl,
41640
+ sourceDomain: row.sourceDomain,
41641
+ targetUrl: row.targetUrl,
41642
+ anchorText: row.anchorText ?? void 0,
41643
+ firstSeenAt: row.firstSeenAt ?? void 0,
41644
+ lastSeenAt: row.lastSeenAt ?? void 0,
41645
+ nofollow: row.dofollow === null ? void 0 : !row.dofollow,
41646
+ linkType: row.linkType ?? void 0,
41647
+ attributes: row.attributes,
41648
+ state: row.state,
41649
+ indirect: row.indirect ?? void 0,
41650
+ linksFromPage: row.linksFromPage ?? void 0,
41651
+ linksFromDomain: row.linksFromDomain ?? void 0,
41652
+ providerMetrics: row.metrics
41653
+ }));
41654
+ const counts = /* @__PURE__ */ new Map();
41655
+ for (const row of rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
41656
+ const targetCounts = [...counts].map(([targetUrl, observedLinks]) => ({
41657
+ targetUrl,
41658
+ observedLinks
41659
+ })).sort((left, right) => right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0));
41660
+ const completeness = backlinks.coverage.completeness;
41661
+ const representative = backlinks.data.mode === "representative";
41662
+ const partial = completeness !== "complete" || representative;
41663
+ return {
41664
+ rows,
41665
+ targetCounts,
41666
+ provenance: {
41667
+ provider: "dataforseo",
41668
+ observedAt: backlinks.observedAt > summary.observedAt ? backlinks.observedAt : summary.observedAt,
41669
+ cached: summary.cache.status === "hit" && backlinks.cache.status === "hit",
41670
+ suppliedRows: backlinks.coverage.returnedRows ?? rows.length,
41671
+ validRows: rows.length,
41672
+ invalidRows: backlinks.coverage.invalidRows,
41673
+ duplicateRows: Math.max(0, (backlinks.coverage.returnedRows ?? rows.length) - backlinks.coverage.invalidRows - rows.length),
41674
+ capped: ["capped", "partial"].includes(completeness),
41675
+ rowLimit: limit,
41676
+ completeness: partial ? "partial" : "complete",
41677
+ providerRequests: {
41678
+ methods: [summary.request.endpoint, backlinks.request.endpoint],
41679
+ maxConcurrentRequests: 1
41680
+ },
41681
+ providerCoverage: {
41682
+ targetCountRows: {
41683
+ returnedRows: targetCounts.length,
41684
+ retainedRows: targetCounts.length,
41685
+ invalidRows: 0
41686
+ },
41687
+ detailRows: {
41688
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
41689
+ retainedRows: rows.length,
41690
+ invalidRows: backlinks.coverage.invalidRows
41691
+ },
41692
+ summaryRows: {
41693
+ returnedRows: summary.coverage.returnedRows ?? 0,
41694
+ retainedRows: summary.coverage.retainedRows ?? 0,
41695
+ invalidRows: summary.coverage.invalidRows
41696
+ },
41697
+ backlinkRows: {
41698
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
41699
+ retainedRows: rows.length,
41700
+ invalidRows: backlinks.coverage.invalidRows,
41701
+ providerTotalRows: backlinks.coverage.providerTotalRows
41702
+ }
41703
+ }
41704
+ },
41705
+ externalProvider: {
41706
+ summary,
41707
+ backlinks
41708
+ },
41709
+ warnings: [
41710
+ ...summary.warnings.map((warning) => warning.message),
41711
+ ...backlinks.warnings.map((warning) => warning.message),
41712
+ ...representative ? ["The link list retains one representative backlink per referring domain. Provider summary counts remain separate from retained rows."] : [],
41713
+ ...!representative && partial ? ["The link list is bounded or partial. Provider summary counts remain separate from retained rows."] : []
41714
+ ]
41715
+ };
41716
+ }
41717
+ //#endregion
40574
41718
  //#region packages/core/src/links/import.ts
40575
41719
  const DEFAULT_ROW_LIMIT$1 = 1e4;
40576
41720
  const MAX_ROW_LIMIT$1 = 1e5;
@@ -40759,6 +41903,10 @@ async function importLinkEvidence(input) {
40759
41903
  //#region packages/core/src/links/report.ts
40760
41904
  const DEFAULT_OUTPUT_LIMIT = 100;
40761
41905
  const MAX_OUTPUT_LIMIT = 500;
41906
+ const MAX_TARGET_COUNTS = 100;
41907
+ const MAX_CONTEXT_ROWS = 100;
41908
+ const MAX_FINDINGS = 50;
41909
+ const MAX_STRUCTURED_DETAIL_ROWS = 750;
40762
41910
  function boundedLimit(value) {
40763
41911
  const limit = value ?? DEFAULT_OUTPUT_LIMIT;
40764
41912
  if (!Number.isInteger(limit) || limit < 1 || limit > MAX_OUTPUT_LIMIT) throw new SeoError("INVALID_INPUT", `Link report limit must be between 1 and ${MAX_OUTPUT_LIMIT}.`);
@@ -40770,7 +41918,7 @@ function targetCounts(rows) {
40770
41918
  return [...counts].map(([targetUrl, observedLinks]) => ({
40771
41919
  targetUrl,
40772
41920
  observedLinks
40773
- })).sort((a, b) => b.observedLinks - a.observedLinks || a.targetUrl.localeCompare(b.targetUrl, "en"));
41921
+ })).sort((a, b) => b.observedLinks - a.observedLinks || (a.targetUrl < b.targetUrl ? -1 : a.targetUrl > b.targetUrl ? 1 : 0));
40774
41922
  }
40775
41923
  function linkEvidenceReport(input) {
40776
41924
  const limit = boundedLimit(input.limit);
@@ -40778,8 +41926,24 @@ function linkEvidenceReport(input) {
40778
41926
  const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
40779
41927
  const observedTargets = targetCounts(input.evidence.rows);
40780
41928
  const counts = input.evidence.targetCounts.length ? input.evidence.targetCounts : observedTargets;
41929
+ const providerEvidence = input.evidence.externalProvider ? {
41930
+ summary: input.evidence.externalProvider.summary,
41931
+ backlinks: {
41932
+ ...input.evidence.externalProvider.backlinks,
41933
+ data: {
41934
+ target: input.evidence.externalProvider.backlinks.data.target,
41935
+ mode: input.evidence.externalProvider.backlinks.data.mode,
41936
+ totalRows: input.evidence.externalProvider.backlinks.data.totalRows,
41937
+ retainedRows: input.evidence.externalProvider.backlinks.data.rows.length
41938
+ }
41939
+ }
41940
+ } : null;
41941
+ const targetCountsOutput = counts.slice(0, MAX_TARGET_COUNTS);
41942
+ const contextRows = input.targetContext?.rows.slice(0, MAX_CONTEXT_ROWS) ?? [];
41943
+ const findings = input.targetContext?.findings.slice(0, MAX_FINDINGS) ?? [];
41944
+ const returnedDetailRows = rows.length + targetCountsOutput.length + contextRows.length + findings.length;
40781
41945
  return {
40782
- schemaVersion: 1,
41946
+ schemaVersion: 2,
40783
41947
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
40784
41948
  dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
40785
41949
  summary: {
@@ -40788,6 +41952,8 @@ function linkEvidenceReport(input) {
40788
41952
  targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
40789
41953
  providerTargetPages: counts.length
40790
41954
  },
41955
+ providerSummary: providerEvidence?.summary.data ?? null,
41956
+ providerEvidence,
40791
41957
  provenance: input.evidence.provenance,
40792
41958
  selection: {
40793
41959
  availableRows: input.evidence.rows.length,
@@ -40795,13 +41961,45 @@ function linkEvidenceReport(input) {
40795
41961
  omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
40796
41962
  limit
40797
41963
  },
40798
- targetCounts: counts.slice(0, 100),
41964
+ outputBudget: {
41965
+ unit: "structured-detail-rows",
41966
+ limit: MAX_STRUCTURED_DETAIL_ROWS,
41967
+ returned: returnedDetailRows,
41968
+ omitted: Math.max(0, input.evidence.rows.length - rows.length + Math.max(0, counts.length - targetCountsOutput.length) + Math.max(0, (input.targetContext?.rows.length ?? 0) - contextRows.length) + Math.max(0, (input.targetContext?.findings.length ?? 0) - findings.length)),
41969
+ sections: {
41970
+ links: {
41971
+ limit: MAX_OUTPUT_LIMIT,
41972
+ returned: rows.length
41973
+ },
41974
+ targetCounts: {
41975
+ limit: MAX_TARGET_COUNTS,
41976
+ returned: targetCountsOutput.length
41977
+ },
41978
+ targetContext: {
41979
+ limit: MAX_CONTEXT_ROWS,
41980
+ returned: contextRows.length
41981
+ },
41982
+ findings: {
41983
+ limit: MAX_FINDINGS,
41984
+ returned: findings.length
41985
+ }
41986
+ }
41987
+ },
41988
+ targetCounts: targetCountsOutput,
40799
41989
  links: rows,
40800
- warnings: input.evidence.warnings,
41990
+ targetContext: input.targetContext ? {
41991
+ ...input.targetContext,
41992
+ rows: contextRows,
41993
+ findings
41994
+ } : null,
41995
+ findings,
41996
+ warnings: [...input.evidence.warnings, ...input.targetContext?.warnings ?? []],
40801
41997
  caveats: [
40802
41998
  "This is bounded provider or imported evidence, not a complete backlink index.",
40803
41999
  "A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
40804
- "Referring-domain counts describe the retained rows only."
42000
+ "Referring-domain counts describe the retained rows only.",
42001
+ ...providerEvidence ? ["Provider profile counts describe its current index. The representative link rows are a separate bounded sample.", "Provider rank and spam metrics keep their provider names and scales. They are context, not ranking factors or universal authority scores."] : [],
42002
+ ...input.targetContext ? ["Search Console page rows are retained first-party evidence. Missing or capped rows are never treated as zero.", "Saved crawl findings describe the recorded crawl time. Verify the live target before changing it."] : []
40805
42003
  ]
40806
42004
  };
40807
42005
  }
@@ -42133,6 +43331,6 @@ function isoDate(daysAgo = 0) {
42133
43331
  return date.toISOString().slice(0, 10);
42134
43332
  }
42135
43333
  //#endregion
42136
- export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
43334
+ export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoLinkProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, collectDataForSeoLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, linkTargetContext, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
42137
43335
 
42138
43336
  //# sourceMappingURL=index.js.map