mcp-scraper 0.38.0 → 0.38.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +813 -338
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +25 -7
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +65 -4
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-PWPUKR5U.js → chunk-AZRPG43B.js} +12 -3
  18. package/dist/chunk-AZRPG43B.js.map +1 -0
  19. package/dist/{chunk-MA5JBAUZ.js → chunk-EGJKUB4Q.js} +3 -3
  20. package/dist/{chunk-MA5JBAUZ.js.map → chunk-EGJKUB4Q.js.map} +1 -1
  21. package/dist/{chunk-PUJFYJXB.js → chunk-FRYT3ID4.js} +2 -2
  22. package/dist/{chunk-CSCD2HNS.js → chunk-N7KUTTCC.js} +312 -101
  23. package/dist/chunk-N7KUTTCC.js.map +1 -0
  24. package/dist/{chunk-LOPKN3YL.js → chunk-NGM237OO.js} +66 -5
  25. package/dist/chunk-NGM237OO.js.map +1 -0
  26. package/dist/{chunk-P64PZUCA.js → chunk-NKCCGADE.js} +23 -8
  27. package/dist/chunk-NKCCGADE.js.map +1 -0
  28. package/dist/{chunk-FSAXLDB3.js → chunk-SFRMFGQ6.js} +2 -2
  29. package/dist/{chunk-Q35WZJJK.js → chunk-YCI2PNCS.js} +3 -3
  30. package/dist/chunk-YODBNTTN.js +7 -0
  31. package/dist/chunk-YODBNTTN.js.map +1 -0
  32. package/dist/{db-N3YECFWR.js → db-C5KVCOYT.js} +10 -4
  33. package/dist/{extract-bundle-346R6MXD.js → extract-bundle-KUBX6N6Z.js} +4 -4
  34. package/dist/index.cjs +65 -4
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/{location-data-repository-O2VII3ON.js → location-data-repository-TTWF3OTM.js} +3 -3
  38. package/dist/{server-3Q6XIJPF.js → server-5EX6XBIA.js} +320 -152
  39. package/dist/server-5EX6XBIA.js.map +1 -0
  40. package/dist/{site-extract-repository-I3VM6WXN.js → site-extract-repository-XSPJTCIL.js} +4 -4
  41. package/dist/{worker-TTFXPPDK.js → worker-XCPU4YSN.js} +6 -6
  42. package/docs/mcp-tool-manifest.generated.json +66 -9
  43. package/package.json +4 -2
  44. package/dist/chunk-CSCD2HNS.js.map +0 -1
  45. package/dist/chunk-H3M3O457.js +0 -7
  46. package/dist/chunk-H3M3O457.js.map +0 -1
  47. package/dist/chunk-LOPKN3YL.js.map +0 -1
  48. package/dist/chunk-P64PZUCA.js.map +0 -1
  49. package/dist/chunk-PWPUKR5U.js.map +0 -1
  50. package/dist/server-3Q6XIJPF.js.map +0 -1
  51. /package/dist/{chunk-PUJFYJXB.js.map → chunk-FRYT3ID4.js.map} +0 -0
  52. /package/dist/{chunk-FSAXLDB3.js.map → chunk-SFRMFGQ6.js.map} +0 -0
  53. /package/dist/{chunk-Q35WZJJK.js.map → chunk-YCI2PNCS.js.map} +0 -0
  54. /package/dist/{db-N3YECFWR.js.map → db-C5KVCOYT.js.map} +0 -0
  55. /package/dist/{extract-bundle-346R6MXD.js.map → extract-bundle-KUBX6N6Z.js.map} +0 -0
  56. /package/dist/{location-data-repository-O2VII3ON.js.map → location-data-repository-TTWF3OTM.js.map} +0 -0
  57. /package/dist/{site-extract-repository-I3VM6WXN.js.map → site-extract-repository-XSPJTCIL.js.map} +0 -0
  58. /package/dist/{worker-TTFXPPDK.js.map → worker-XCPU4YSN.js.map} +0 -0
@@ -9,13 +9,13 @@ import {
9
9
  importHostedCensusPlacesCsv,
10
10
  importHostedZipGroupsCsv,
11
11
  listActiveHostedLocationDatasets
12
- } from "./chunk-PUJFYJXB.js";
12
+ } from "./chunk-FRYT3ID4.js";
13
13
  import {
14
14
  classifyHarvestProblem,
15
15
  createHarvestAttemptRecorder,
16
16
  harvestProblemResponse,
17
17
  serializeHarvestProblem
18
- } from "./chunk-FSAXLDB3.js";
18
+ } from "./chunk-SFRMFGQ6.js";
19
19
  import {
20
20
  BACKUP_LAST_RESORT_ATTEMPTS,
21
21
  BrowserDriver,
@@ -29,7 +29,7 @@ import {
29
29
  resolveBackupKernelProxyId,
30
30
  resolveKernelProxyId,
31
31
  serpArrivalUrl
32
- } from "./chunk-LOPKN3YL.js";
32
+ } from "./chunk-NGM237OO.js";
33
33
  import {
34
34
  HttpMcpToolExecutor,
35
35
  WIZPER_LANGUAGES,
@@ -56,7 +56,7 @@ import {
56
56
  sanitizeAttempts,
57
57
  sanitizeHarvestResult,
58
58
  transcribeMediaUrl
59
- } from "./chunk-P64PZUCA.js";
59
+ } from "./chunk-NKCCGADE.js";
60
60
  import {
61
61
  auditImageUrls,
62
62
  auditImages,
@@ -74,7 +74,7 @@ import {
74
74
  recordKernelSession,
75
75
  recordVendorUsage,
76
76
  runWithCostContext
77
- } from "./chunk-MA5JBAUZ.js";
77
+ } from "./chunk-EGJKUB4Q.js";
78
78
  import {
79
79
  listWorkflowDefinitions,
80
80
  runWorkflow,
@@ -101,7 +101,7 @@ import {
101
101
  RawMapsOverviewSchema,
102
102
  RawMapsReviewStatsSchema
103
103
  } from "./chunk-CB5C3BPB.js";
104
- import "./chunk-H3M3O457.js";
104
+ import "./chunk-YODBNTTN.js";
105
105
  import {
106
106
  abandonExtractSettlement,
107
107
  countSuccessfulPages,
@@ -124,7 +124,7 @@ import {
124
124
  setExtractJobTotal,
125
125
  settleExtractJob,
126
126
  terminalExtractJobStatus
127
- } from "./chunk-Q35WZJJK.js";
127
+ } from "./chunk-YCI2PNCS.js";
128
128
  import {
129
129
  BROWSER_OPEN_MIN_BALANCE_MC,
130
130
  CONCURRENCY_PRICE_ID,
@@ -169,10 +169,9 @@ import {
169
169
  tierBillingInterval,
170
170
  tierCreditsPerInterval,
171
171
  vendorCostUsdToMc
172
- } from "./chunk-PWPUKR5U.js";
172
+ } from "./chunk-AZRPG43B.js";
173
173
  import {
174
174
  CaptchaError,
175
- ExtractionError,
176
175
  MapsSearchIncompletePageError,
177
176
  RECAPTCHA_INSTRUCTIONS,
178
177
  sanitizeVendorName
@@ -183,7 +182,9 @@ import {
183
182
  appendWorkflowArtifacts,
184
183
  cancelJob,
185
184
  checkRateLimit,
185
+ claimStripeEvent,
186
186
  completeJob,
187
+ completeStripeEvent,
187
188
  completeWorkflowRunRecord,
188
189
  consumeCode,
189
190
  consumePasswordResetToken,
@@ -202,6 +203,7 @@ import {
202
203
  deleteWorkflowSchedule,
203
204
  expireOldLots,
204
205
  failJob,
206
+ failStripeEvent,
205
207
  failWorkflowRunRecord,
206
208
  getBillingLedgerSummary,
207
209
  getClient,
@@ -245,7 +247,6 @@ import {
245
247
  putCode,
246
248
  putRefresh,
247
249
  reconcileBalanceMc,
248
- recordStripeEvent,
249
250
  recordWorkflowWebhookDelivery,
250
251
  registerClient,
251
252
  replaceWorkflowArtifacts,
@@ -265,7 +266,7 @@ import {
265
266
  softDeleteInboxMessage,
266
267
  upsertPageSnapshot,
267
268
  verifyPassword
268
- } from "./chunk-CSCD2HNS.js";
269
+ } from "./chunk-N7KUTTCC.js";
269
270
 
270
271
  // src/blog/registry.ts
271
272
  var posts = [
@@ -4688,12 +4689,15 @@ async function spiderSite(opts) {
4688
4689
  const discovered = { url, status, linkedFrom: from, via: "fetch" };
4689
4690
  const children = [];
4690
4691
  if (html) {
4691
- for (const link of extractLinks(html, url)) {
4692
- if (baseDomain(link) !== startDomain) continue;
4693
- if (!visited.has(link)) {
4694
- visited.add(link);
4695
- children.push({ url: link, from: url });
4692
+ try {
4693
+ for (const link of extractLinks(html, url)) {
4694
+ if (baseDomain(link) !== startDomain) continue;
4695
+ if (!visited.has(link)) {
4696
+ visited.add(link);
4697
+ children.push({ url: link, from: url });
4698
+ }
4696
4699
  }
4700
+ } catch {
4697
4701
  }
4698
4702
  }
4699
4703
  return { discovered, children };
@@ -6100,67 +6104,78 @@ async function extractSite(opts) {
6100
6104
  let truncated = false;
6101
6105
  let spider;
6102
6106
  let sitemapUrls = [];
6103
- if (rotating) {
6104
- const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
6105
- const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
6106
- const followLinks = !seeded;
6107
- const seen = /* @__PURE__ */ new Set();
6108
- const frontier = [];
6109
- const enqueue = (u) => {
6110
- const n = normalizeUrl(u, opts.startUrl);
6111
- if (n && !seen.has(n)) {
6112
- seen.add(n);
6113
- frontier.push(n);
6114
- }
6115
- };
6116
- if (seeded) {
6117
- for (const u of opts.seedUrls) enqueue(u);
6118
- } else {
6119
- enqueue(opts.startUrl);
6120
- sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
6121
- for (const u of sitemapUrls) enqueue(u);
6122
- }
6123
- const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
6124
- let done = 0;
6125
- while (frontier.length > 0 && pages.length < maxPages) {
6126
- const capacity = maxPages - pages.length;
6127
- const wave = frontier.splice(0, Math.min(capacity, waveCap));
6128
- const fetched = await crawlWithRotation(wave, {
6129
- apiKey: opts.kernelApiKey,
6130
- concurrency: browserConcurrency,
6131
- urlsPerBrowser,
6132
- onPage: (p) => {
6133
- done++;
6134
- opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
6107
+ let stoppedEarly = null;
6108
+ try {
6109
+ if (rotating) {
6110
+ const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
6111
+ const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
6112
+ const followLinks = !seeded;
6113
+ const seen = /* @__PURE__ */ new Set();
6114
+ const frontier = [];
6115
+ const enqueue = (u) => {
6116
+ const n = normalizeUrl(u, opts.startUrl);
6117
+ if (n && !seen.has(n)) {
6118
+ seen.add(n);
6119
+ frontier.push(n);
6135
6120
  }
6136
- });
6137
- for (const r of fetched) {
6138
- const page = r.html && r.status >= 200 && r.status < 300 ? parsePageData(r.url, r.html, r.status, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }, [successfulAttempt("browser", r.status, r.responseTimeMs ?? null)]) : emptyPageData(r.url, r.status || null, "browser", {
6139
- code: r.errorCode ?? "browser_empty_response",
6140
- message: r.error ?? "Browser fetch returned no usable response.",
6141
- attempts: [failedAttempt("browser", r.status || null, r.responseTimeMs ?? null, r.errorCode ?? "browser_empty_response", r.error ?? "Browser fetch returned no usable response.")]
6121
+ };
6122
+ if (seeded) {
6123
+ for (const u of opts.seedUrls) enqueue(u);
6124
+ } else {
6125
+ enqueue(opts.startUrl);
6126
+ sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
6127
+ for (const u of sitemapUrls) enqueue(u);
6128
+ }
6129
+ const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
6130
+ let done = 0;
6131
+ while (frontier.length > 0 && pages.length < maxPages) {
6132
+ const capacity = maxPages - pages.length;
6133
+ const wave = frontier.splice(0, Math.min(capacity, waveCap));
6134
+ const fetched = await crawlWithRotation(wave, {
6135
+ apiKey: opts.kernelApiKey,
6136
+ concurrency: browserConcurrency,
6137
+ urlsPerBrowser,
6138
+ onPage: (p) => {
6139
+ done++;
6140
+ opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
6141
+ }
6142
6142
  });
6143
- pages.push(page);
6144
- browserRetries++;
6145
- if (followLinks && isPageExtractionSuccessful(page)) {
6146
- for (const href of page.discoveryLinks ?? page.outlinks.filter((link) => link.internal).map((link) => link.href)) enqueue(href);
6143
+ for (const r of fetched) {
6144
+ const page = r.html && r.status >= 200 && r.status < 300 ? parsePageData(r.url, r.html, r.status, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }, [successfulAttempt("browser", r.status, r.responseTimeMs ?? null)]) : emptyPageData(r.url, r.status || null, "browser", {
6145
+ code: r.errorCode ?? "browser_empty_response",
6146
+ message: r.error ?? "Browser fetch returned no usable response.",
6147
+ attempts: [failedAttempt("browser", r.status || null, r.responseTimeMs ?? null, r.errorCode ?? "browser_empty_response", r.error ?? "Browser fetch returned no usable response.")]
6148
+ });
6149
+ pages.push(page);
6150
+ browserRetries++;
6151
+ if (followLinks && isPageExtractionSuccessful(page)) {
6152
+ for (const href of page.discoveryLinks ?? page.outlinks.filter((link) => link.internal).map((link) => link.href)) enqueue(href);
6153
+ }
6147
6154
  }
6148
6155
  }
6156
+ truncated = frontier.length > 0;
6157
+ spider = makeSeedSpider(opts.startUrl, [...seen]);
6158
+ } else {
6159
+ spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
6160
+ sitemapUrls = spider.sitemapUrls;
6161
+ const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
6162
+ browserRetries = spider.browserRetries;
6163
+ await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
6164
+ const data = await fetchAndParse(url, opts.kernelApiKey);
6165
+ if (data.via === "browser") browserRetries++;
6166
+ pages.push(data);
6167
+ });
6168
+ truncated = spider.truncated || pages.length >= maxPages;
6149
6169
  }
6150
- truncated = frontier.length > 0;
6151
- spider = makeSeedSpider(opts.startUrl, [...seen]);
6152
- } else {
6153
- spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
6154
- sitemapUrls = spider.sitemapUrls;
6155
- const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
6156
- browserRetries = spider.browserRetries;
6157
- await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
6158
- const data = await fetchAndParse(url, opts.kernelApiKey);
6159
- if (data.via === "browser") browserRetries++;
6160
- pages.push(data);
6161
- });
6162
- truncated = spider.truncated || pages.length >= maxPages;
6170
+ } catch (err) {
6171
+ if (pages.length === 0) throw err;
6172
+ stoppedEarly = {
6173
+ code: "crawl_stopped_early",
6174
+ message: err instanceof Error ? err.message : String(err)
6175
+ };
6176
+ truncated = true;
6163
6177
  }
6178
+ if (!spider) spider = makeSeedSpider(opts.startUrl, pages.map((p) => p.url));
6164
6179
  const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
6165
6180
  for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
6166
6181
  let branding = null;
@@ -6170,10 +6185,11 @@ async function extractSite(opts) {
6170
6185
  const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
6171
6186
  let imageAudit = null;
6172
6187
  if (opts.formats?.includes("images")) {
6173
- imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
6188
+ imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 }).catch(() => null);
6174
6189
  }
6175
6190
  return {
6176
6191
  startUrl: opts.startUrl,
6192
+ stoppedEarly,
6177
6193
  pages,
6178
6194
  totalPages: pages.length,
6179
6195
  spider,
@@ -8595,7 +8611,7 @@ var siteExtractFn = inngest.createFunction(
8595
8611
  });
8596
8612
  }) : null;
8597
8613
  const finalized = await step.run("finalize", async () => {
8598
- const { assembleExtractArtifacts } = await import("./extract-bundle-346R6MXD.js");
8614
+ const { assembleExtractArtifacts } = await import("./extract-bundle-KUBX6N6Z.js");
8599
8615
  const current = await getExtractJob(jobId2);
8600
8616
  if (!current) throw new Error("extract job disappeared before finalization");
8601
8617
  const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
@@ -8698,6 +8714,30 @@ function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
8698
8714
  close?.click();
8699
8715
  return Boolean(close);
8700
8716
  }
8717
+ function readExpandedPanelRowInBrowser(input) {
8718
+ const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
8719
+ const expectedBusiness = normalize(input.businessName).toLowerCase();
8720
+ const visible = (element) => element.getAttribute("aria-hidden") !== "true" && (element.offsetParent !== null || element.getClientRects().length > 0);
8721
+ const readText = (element) => normalize(element.innerText || element.textContent);
8722
+ const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => visible(dialog) && readText(dialog).toLowerCase().includes(expectedBusiness)).reverse();
8723
+ for (const dialog of dialogs) {
8724
+ const row = Array.from(
8725
+ dialog.querySelectorAll('button, a, [role="button"]')
8726
+ ).find((element) => visible(element) && readText(element).startsWith(input.prefix));
8727
+ if (!row) continue;
8728
+ const body = readText(row).slice(input.prefix.length);
8729
+ const seen = /* @__PURE__ */ new Set();
8730
+ const items = [];
8731
+ for (const part of body.split(/\s*[,\u00b7]\s*/)) {
8732
+ const value = part.trim();
8733
+ if (!value || seen.has(value)) continue;
8734
+ seen.add(value);
8735
+ items.push(value);
8736
+ }
8737
+ if (items.length > 0) return items;
8738
+ }
8739
+ return [];
8740
+ }
8701
8741
  async function clickPanelRowWithRetry(page, businessName, prefix) {
8702
8742
  const input = { businessName, prefix };
8703
8743
  if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
@@ -8711,9 +8751,12 @@ async function extractExpandedList(page, businessName, rowPrefix, dialogHeading)
8711
8751
  extractOpenBusinessPanelDialogItemsInBrowser,
8712
8752
  dialogHeading
8713
8753
  );
8714
- await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
8715
- await page.waitForTimeout(250);
8716
- return items;
8754
+ if (items.length > 0) {
8755
+ await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
8756
+ await page.waitForTimeout(250);
8757
+ return items;
8758
+ }
8759
+ return page.evaluate(readExpandedPanelRowInBrowser, { businessName, prefix: rowPrefix });
8717
8760
  }
8718
8761
  async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
8719
8762
  const services = await extractExpandedList(page, businessName, "Services:", "Services");
@@ -8778,7 +8821,7 @@ var MapsSearchExtractor = class {
8778
8821
  await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
8779
8822
  if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
8780
8823
  const reachedFinder = await this.clickIntoLocalFinder(page);
8781
- if (!reachedFinder) throw new ExtractionError('Google "More businesses" control was not found on the organic results page');
8824
+ if (!reachedFinder) throw new MapsSearchIncompletePageError('Google "More businesses" control was not found on the organic results page');
8782
8825
  await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
8783
8826
  if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
8784
8827
  const results = await this.collectResults(page, options);
@@ -9043,6 +9086,7 @@ var MapsSearchExtractor = class {
9043
9086
  };
9044
9087
 
9045
9088
  // src/maps-search-rotation.ts
9089
+ var UNCONFIRMED_PAGE_MAX_RETRIES = 5;
9046
9090
  var MAPS_SEARCH_MAX_ATTEMPTS = 3;
9047
9091
  var MAPS_SEARCH_NONE_MAX_ATTEMPTS = 7;
9048
9092
  var MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS = 5;
@@ -9110,7 +9154,8 @@ function shouldRetryMapsSearchError(input) {
9110
9154
  if (input.attemptIndex >= input.maxAttempts - 1) return false;
9111
9155
  if (!retryableMapsSearchError(input.err, input.proxyMode)) return false;
9112
9156
  if (input.err instanceof MapsSearchIncompletePageError) {
9113
- return !input.priorAttempts.some((attempt) => attempt.outcome === "results_unconfirmed");
9157
+ const unconfirmed = input.priorAttempts.filter((attempt) => attempt.outcome === "results_unconfirmed").length;
9158
+ return unconfirmed < UNCONFIRMED_PAGE_MAX_RETRIES;
9114
9159
  }
9115
9160
  return true;
9116
9161
  }
@@ -9508,7 +9553,7 @@ async function resolveUsZipsPath(requestedPath) {
9508
9553
  async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
9509
9554
  const mode = resolvedLocationDataMode({ zipSource: sourceMode });
9510
9555
  if (mode === "hosted") {
9511
- const { getHostedZipGroups } = await import("./location-data-repository-O2VII3ON.js");
9556
+ const { getHostedZipGroups } = await import("./location-data-repository-TTWF3OTM.js");
9512
9557
  const hosted = await getHostedZipGroups(stateAbbr);
9513
9558
  if (!hosted.dataset) {
9514
9559
  throw new DirectoryLocationError(
@@ -9571,7 +9616,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
9571
9616
  const sourceUrl = censusStateUrl(state.fips);
9572
9617
  const warnings = [];
9573
9618
  if (resolvedLocationDataMode(config) === "hosted") {
9574
- const { queryHostedLocationMarkets } = await import("./location-data-repository-O2VII3ON.js");
9619
+ const { queryHostedLocationMarkets } = await import("./location-data-repository-TTWF3OTM.js");
9575
9620
  const hosted = await queryHostedLocationMarkets({
9576
9621
  state: state.abbr,
9577
9622
  minPopulation: options.minPopulation,
@@ -13718,6 +13763,7 @@ async function resolveYouTubeAudio(videoId) {
13718
13763
  if (!base || !secret2) return null;
13719
13764
  const controller = new AbortController();
13720
13765
  const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
13766
+ const startedMs = Date.now();
13721
13767
  try {
13722
13768
  const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
13723
13769
  method: "POST",
@@ -13735,6 +13781,12 @@ async function resolveYouTubeAudio(videoId) {
13735
13781
  console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
13736
13782
  return null;
13737
13783
  }
13784
+ void recordVendorUsage({
13785
+ vendor: "frames_audio_resolve",
13786
+ model: null,
13787
+ units: (Date.now() - startedMs) / 1e3,
13788
+ unitType: "compute_sec"
13789
+ });
13738
13790
  return {
13739
13791
  mediaUrl: data.mediaUrl,
13740
13792
  durationS: Number(data.durationS) || 0,
@@ -15232,10 +15284,23 @@ function isAllowedFacebookOrganicUrl(url) {
15232
15284
  async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
15233
15285
  return transcribeMediaUrl(videoUrl, markdownTitle);
15234
15286
  }
15287
+ var AD_LIBRARY_APP_CHROME = /Ad Library|Search by keyword|Branded Content/i;
15288
+ var AUTH_WALL_URL = /facebook\.com\/(login|checkpoint|authentication)/i;
15289
+ var MIN_RENDERED_BODY_CHARS = 200;
15290
+ function classifyAdLibraryPage(bodyText, finalUrl) {
15291
+ if (AUTH_WALL_URL.test(finalUrl)) return "blocked";
15292
+ if (bodyText.length < MIN_RENDERED_BODY_CHARS) return "blocked";
15293
+ return AD_LIBRARY_APP_CHROME.test(bodyText) ? "reached_app" : "blocked";
15294
+ }
15295
+ async function refundEmptyPageIntel(c, userId, subject, blocked) {
15296
+ await creditMc(userId, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, blocked ? "soft-block empty result" : "empty result");
15297
+ await logRequestEvent({ userId, source: "facebook_page_intel", status: "failed", query: subject, error: blocked ? "soft-block: empty result refunded" : "empty result refunded" });
15298
+ return blocked ? c.json({ error: "soft-block: no ads returned (refunded)" }, 503) : c.json({ error: `No ads in Meta's Ad Library match "${subject}" (refunded). This is a real empty result, not a block.` }, 404);
15299
+ }
15235
15300
  async function detectSoftBlock(driver) {
15236
15301
  const page = driver.getPage();
15237
15302
  const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
15238
- return bodyText.length < 200 || /Log in|log in|Create new account|You must log in/.test(bodyText);
15303
+ return classifyAdLibraryPage(bodyText, page.url()) === "blocked";
15239
15304
  }
15240
15305
  function buildPageIntelUrl(body, country) {
15241
15306
  if (body.libraryId?.trim()) return `https://www.facebook.com/ads/library/?id=${FacebookAdExtractor.resolveLibraryId(body.libraryId.trim()) ?? body.libraryId.trim()}`;
@@ -15371,26 +15436,27 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
15371
15436
  const driver = new BrowserDriver();
15372
15437
  let refunded = false, debited = false;
15373
15438
  try {
15374
- const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD, subject);
15375
- if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_ad), 402);
15439
+ const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL, subject);
15440
+ if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_page_intel), 402);
15376
15441
  debited = true;
15377
15442
  await driver.launch(await kernelLaunchOptsResidential());
15378
15443
  await driver.navigateTo(listingUrl);
15379
15444
  const extractor = new FacebookAdExtractor(driver);
15380
15445
  const result = await extractor.extractPageIntel(listingUrl, maxAds);
15381
- if (result.ads.length === 0 && await detectSoftBlock(driver)) {
15382
- await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
15446
+ if (result.ads.length === 0) {
15383
15447
  refunded = true;
15384
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: "soft-block: empty result refunded" });
15385
- return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
15448
+ return await refundEmptyPageIntel(c, fbUser.id, subject, await detectSoftBlock(driver));
15386
15449
  }
15450
+ const perAdMc = result.ads.length * MC_COSTS.fb_page_intel_ad;
15451
+ const adDebit = await debitMc(fbUser.id, perAdMc, LedgerOperation.FB_PAGE_INTEL_AD, subject);
15452
+ if (!adDebit.ok) return c.json(insufficientBalanceResponse(adDebit.balance_mc, perAdMc), 402);
15387
15453
  const match = assessPageIntelMatch(body, result);
15388
15454
  const output = { ...result, ...match };
15389
15455
  await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
15390
15456
  return c.json(output);
15391
15457
  } catch (err) {
15392
15458
  const msg = err instanceof Error ? err.message : String(err);
15393
- if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
15459
+ if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, "failed call");
15394
15460
  await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
15395
15461
  if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
15396
15462
  return c.json({ error: msg }, 503);
@@ -15531,10 +15597,16 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
15531
15597
  debited = true;
15532
15598
  await driver.launch(await kernelLaunchOptsResidential());
15533
15599
  const page = driver.getPage();
15534
- const collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
15600
+ let collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
15601
+ if (collated.length === 0) {
15602
+ collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
15603
+ }
15535
15604
  const gqlAdvertisers = advertisersFromResults(collated, maxResults);
15536
15605
  if (gqlAdvertisers.length > 0) {
15537
15606
  const results2 = gqlAdvertisers.map((a) => ({ name: a.pageName, pageName: a.pageName, pageId: a.pageId, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
15607
+ const gqlPerResultMc = results2.length * MC_COSTS.fb_search_result;
15608
+ const gqlResultDebit = await debitMc(fbUser.id, gqlPerResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
15609
+ if (!gqlResultDebit.ok) return c.json(insufficientBalanceResponse(gqlResultDebit.balance_mc, gqlPerResultMc), 402);
15538
15610
  const searchResult2 = { query: body.query.trim(), searchUrl, results: results2, via: "graphql" };
15539
15611
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results2.length, result: searchResult2 });
15540
15612
  return c.json(searchResult2);
@@ -15547,7 +15619,7 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
15547
15619
  await page.waitForTimeout(1e3);
15548
15620
  }
15549
15621
  const rawBodyText = await page.evaluate(() => document.body?.innerText ?? "");
15550
- if (rawBodyText.length < 200 || /Log in|You must log in|Create new account/.test(rawBodyText)) {
15622
+ if (classifyAdLibraryPage(rawBodyText, page.url()) === "blocked") {
15551
15623
  await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "soft-block empty result");
15552
15624
  searchRefunded = true;
15553
15625
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "soft-block: empty result refunded" });
@@ -15588,8 +15660,11 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
15588
15660
  await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
15589
15661
  searchRefunded = true;
15590
15662
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
15591
- return c.json({ error: "no advertisers found (refunded)" }, 503);
15663
+ return c.json({ error: `No advertisers in Meta's Ad Library match "${body.query.trim()}" (refunded). This is a real empty result, not a block \u2014 try a shorter or broader query, or a brand/page name.` }, 404);
15592
15664
  }
15665
+ const perResultMc = results.length * MC_COSTS.fb_search_result;
15666
+ const resultDebit = await debitMc(fbUser.id, perResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
15667
+ if (!resultDebit.ok) return c.json(insufficientBalanceResponse(resultDebit.balance_mc, perResultMc), 402);
15593
15668
  const searchResult = { query: body.query.trim(), searchUrl, results };
15594
15669
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
15595
15670
  return c.json(searchResult);
@@ -15658,9 +15733,6 @@ import { z as z18 } from "zod";
15658
15733
 
15659
15734
  // src/extractor/GoogleAdsExtractor.ts
15660
15735
  var ORIGIN = "https://adstransparency.google.com";
15661
- function looksLikeDomain(q) {
15662
- return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(q.trim()) && !q.includes(" ");
15663
- }
15664
15736
  var GoogleAdsExtractor = class {
15665
15737
  constructor(driver) {
15666
15738
  this.driver = driver;
@@ -15671,50 +15743,108 @@ var GoogleAdsExtractor = class {
15671
15743
  }
15672
15744
  async search(query, region, maxResults) {
15673
15745
  const q = query.trim();
15674
- const url = looksLikeDomain(q) ? `${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(q)}` : `${ORIGIN}/?region=${encodeURIComponent(region)}&query=${encodeURIComponent(q)}`;
15675
- await this.driver.navigateTo(url);
15746
+ await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
15676
15747
  const page = this.page();
15677
- await page.waitForTimeout(3500);
15678
- await this.autoScroll(page, 6);
15679
- const advertisers = await page.evaluate((max) => {
15680
- const map = /* @__PURE__ */ new Map();
15681
- const links = Array.from(document.querySelectorAll('a[href*="/advertiser/"]'));
15682
- for (const a of links) {
15683
- const m = a.getAttribute("href")?.match(/\/advertiser\/(AR[0-9]+)/);
15684
- if (!m) continue;
15685
- const id = m[1];
15686
- const card = a.closest("creative-preview, advertiser-preview") || a.parentElement;
15687
- const nameEl = card?.querySelector('[class*="advertiser"], [class*="name"]');
15688
- const name = (nameEl?.innerText || "").trim() || null;
15689
- const cur = map.get(id);
15690
- if (cur) cur.count++;
15691
- else map.set(id, { advertiserId: id, name, count: 1 });
15692
- }
15693
- return Array.from(map.values()).slice(0, max).map((v) => ({
15694
- advertiserId: v.advertiserId,
15695
- name: v.name,
15696
- approxAdCount: v.count
15697
- }));
15698
- }, maxResults);
15748
+ await page.waitForTimeout(2500);
15749
+ const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
15750
+ await box.click();
15751
+ await box.fill("");
15752
+ await box.type(q, { delay: 110 });
15753
+ await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
15754
+ await page.waitForTimeout(2200);
15755
+ const raw = await page.evaluate(() => {
15756
+ const rows = Array.from(document.querySelectorAll('[role="option"]')).map((el2) => {
15757
+ const parts = Array.from(el2.querySelectorAll("*")).filter((n) => n.children.length === 0).map((n) => (n.textContent ?? "").replace(/\s+/g, " ").trim()).filter(Boolean);
15758
+ const flat = el2.innerText.replace(/\s+/g, " ").trim();
15759
+ return { parts: parts.length ? parts : [flat], flat };
15760
+ }).filter((r) => r.flat);
15761
+ return { rows };
15762
+ });
15763
+ const domainRe = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)+$/i;
15764
+ const websiteRows = raw.rows.filter((r) => domainRe.test(r.flat));
15765
+ const advertiserRows = raw.rows.filter((r) => !domainRe.test(r.flat));
15766
+ const advertisers = advertiserRows.slice(0, maxResults).map((row) => {
15767
+ const segments = row.parts.filter((p) => !/^Verified$/i.test(p));
15768
+ const adSeg = segments.find((p) => /~\s*[\d,]+\s*ads?/i.test(p));
15769
+ const adCount = adSeg?.match(/~\s*([\d,]+)\s*ads?/i);
15770
+ const rest = segments.filter((p) => p !== adSeg);
15771
+ return {
15772
+ advertiserId: null,
15773
+ name: rest[0] ?? null,
15774
+ basedIn: rest.length > 1 ? rest[rest.length - 1] : null,
15775
+ domain: null,
15776
+ approxAdCount: adCount ? Number(adCount[1].replace(/,/g, "")) : null,
15777
+ detailUrl: null
15778
+ };
15779
+ });
15780
+ const websites = websiteRows.slice(0, maxResults).map((row) => ({
15781
+ domain: row.flat,
15782
+ detailUrl: `${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(row.flat)}`
15783
+ }));
15699
15784
  return {
15700
15785
  query: q,
15701
15786
  region,
15702
15787
  advertiserCount: advertisers.length,
15703
- advertisers: advertisers.map((a) => ({
15704
- advertiserId: a.advertiserId,
15705
- name: a.name,
15706
- domain: looksLikeDomain(q) ? q : null,
15707
- approxAdCount: a.approxAdCount,
15708
- detailUrl: `${ORIGIN}/advertiser/${a.advertiserId}?region=${region}`
15709
- }))
15788
+ advertisers,
15789
+ websiteCount: websites.length,
15790
+ websites
15710
15791
  };
15711
15792
  }
15793
+ async advertiserIdOnPage() {
15794
+ const page = this.page();
15795
+ const fromUrl = page.url().match(/\/advertiser\/(AR[0-9]+)/);
15796
+ if (fromUrl) return fromUrl[1];
15797
+ return await page.evaluate(() => {
15798
+ for (const a of Array.from(document.querySelectorAll('a[href*="/advertiser/"]'))) {
15799
+ const m = a.getAttribute("href")?.match(/\/advertiser\/(AR[0-9]+)/);
15800
+ if (m) return m[1];
15801
+ }
15802
+ return null;
15803
+ });
15804
+ }
15805
+ async resolveAdvertiserIdByDomain(domain, region) {
15806
+ await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(domain.trim())}`);
15807
+ await this.page().waitForTimeout(3500);
15808
+ return await this.advertiserIdOnPage();
15809
+ }
15810
+ async resolveAdvertiserIdByTypeahead(term, region) {
15811
+ const page = this.page();
15812
+ await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
15813
+ await page.waitForTimeout(2500);
15814
+ const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
15815
+ await box.click();
15816
+ await box.fill("");
15817
+ await box.type(term.trim(), { delay: 110 });
15818
+ await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
15819
+ await page.waitForTimeout(2e3);
15820
+ const optionCount = (await page.$$('[role="option"]')).length;
15821
+ for (let i = 0; i < Math.min(optionCount, 3); i++) {
15822
+ const options = await page.$$('[role="option"]');
15823
+ if (!options[i]) break;
15824
+ await options[i].click().catch(() => null);
15825
+ await page.waitForTimeout(3e3);
15826
+ const found = await this.advertiserIdOnPage();
15827
+ if (found) return found;
15828
+ await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
15829
+ await page.waitForTimeout(1500);
15830
+ const retryBox = await page.waitForSelector('input[type="text"], input', { timeout: 15e3 }).catch(() => null);
15831
+ if (!retryBox) break;
15832
+ await retryBox.click();
15833
+ await retryBox.fill("");
15834
+ await retryBox.type(term.trim(), { delay: 110 });
15835
+ await page.waitForSelector('[role="option"]', { timeout: 15e3 }).catch(() => null);
15836
+ await page.waitForTimeout(1800);
15837
+ }
15838
+ return null;
15839
+ }
15712
15840
  async pageIntel(opts) {
15713
15841
  const { region, maxAds } = opts;
15714
15842
  let advertiserId = opts.advertiserId?.trim() || null;
15715
- if (!advertiserId && opts.domain) {
15716
- const s = await this.search(opts.domain, region, 1);
15717
- advertiserId = s.advertisers[0]?.advertiserId ?? null;
15843
+ if (!advertiserId && opts.domain?.trim()) {
15844
+ advertiserId = await this.resolveAdvertiserIdByDomain(opts.domain, region);
15845
+ }
15846
+ if (!advertiserId && opts.advertiserName?.trim()) {
15847
+ advertiserId = await this.resolveAdvertiserIdByTypeahead(opts.advertiserName, region);
15718
15848
  }
15719
15849
  if (!advertiserId) {
15720
15850
  return { advertiserId: null, advertiserName: null, domain: opts.domain ?? null, region, totalCreatives: 0, videoCount: 0, imageCount: 0, textCount: 0, ads: [] };
@@ -15831,10 +15961,11 @@ var GoogleAdsSearchBodySchema = z18.object({
15831
15961
  });
15832
15962
  var GoogleAdsPageIntelBodySchema = z18.object({
15833
15963
  advertiserId: z18.string().trim().optional(),
15964
+ advertiserName: z18.string().trim().optional(),
15834
15965
  domain: z18.string().trim().optional(),
15835
15966
  region: z18.string().trim().toUpperCase().length(2).optional(),
15836
15967
  maxAds: z18.number().int().min(1).max(200).optional()
15837
- }).refine((d) => !!d.advertiserId || !!d.domain, { message: "advertiserId or domain is required" });
15968
+ }).refine((d) => !!d.advertiserId || !!d.domain || !!d.advertiserName, { message: "advertiserId, advertiserName, or domain is required (advertiserName and domain both come straight from google_ads_search)" });
15838
15969
  var GoogleAdsTranscribeBodySchema = z18.object({
15839
15970
  videoUrl: z18.string().trim().min(1, "videoUrl is required")
15840
15971
  });
@@ -15884,13 +16015,13 @@ googleAdsApp.post("/search", createApiKeyAuth(), async (c) => {
15884
16015
  const extractor = new GoogleAdsExtractor(driver);
15885
16016
  return extractor.search(body.query.trim(), region, maxResults);
15886
16017
  });
15887
- if (result.advertisers.length === 0) {
16018
+ if (result.advertisers.length === 0 && result.websites.length === 0) {
15888
16019
  await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
15889
16020
  refunded = true;
15890
16021
  await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
15891
16022
  return c.json({ error: "no advertisers found (refunded)" }, 503);
15892
16023
  }
15893
- await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length, result });
16024
+ await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length + result.websites.length, result });
15894
16025
  return c.json(result);
15895
16026
  } catch (err) {
15896
16027
  const msg = err instanceof Error ? err.message : String(err);
@@ -15909,7 +16040,7 @@ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
15909
16040
  const body = parsed.data;
15910
16041
  const region = body.region ?? "US";
15911
16042
  const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
15912
- const queryLabel = body.advertiserId ?? body.domain ?? "";
16043
+ const queryLabel = body.advertiserId ?? body.domain ?? body.advertiserName ?? "";
15913
16044
  const user = c.get("user");
15914
16045
  const gate = await acquireConcurrencyGate(user, "google_ads_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { advertiserId: body.advertiserId, domain: body.domain, region } });
15915
16046
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
@@ -15922,7 +16053,7 @@ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
15922
16053
  const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
15923
16054
  await driver.launch(await kernelLaunchOptsResidential2());
15924
16055
  const extractor = new GoogleAdsExtractor(driver);
15925
- return extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
16056
+ return extractor.pageIntel({ advertiserId: body.advertiserId, advertiserName: body.advertiserName, domain: body.domain, region, maxAds });
15926
16057
  });
15927
16058
  if (result.ads.length === 0) {
15928
16059
  await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
@@ -16893,6 +17024,12 @@ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
16893
17024
  scrollDelayMs: body.scrollDelayMs,
16894
17025
  stableScrollLimit: body.stableScrollLimit
16895
17026
  });
17027
+ if (result.items.length === 0) {
17028
+ await creditMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE_REFUND, "empty result");
17029
+ debited = false;
17030
+ await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "failed", query: target.profileUrl, error: "empty result refunded" });
17031
+ return c.json({ error: `No public posts were readable on ${target.profileUrl} (refunded). The profile may be private, empty, or require a logged-in session.` }, 404);
17032
+ }
16896
17033
  const response = { ...result, browser: launch.browser };
16897
17034
  await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "done", query: target.profileUrl, resultCount: result.items.length, result: response });
16898
17035
  return c.json(response);
@@ -17016,7 +17153,10 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
17016
17153
  const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
17017
17154
  if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
17018
17155
  transcriptDebited = true;
17019
- transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
17156
+ transcript = await runWithCostContext(
17157
+ { ...currentCostContext(), op: "instagram_transcribe", userId: user.id },
17158
+ () => transcribeMediaUrl(audioUrl, "# Instagram Media Transcript")
17159
+ );
17020
17160
  const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
17021
17161
  const diff = holdMc - settleMc;
17022
17162
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
@@ -18454,6 +18594,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
18454
18594
  const driver = new BrowserDriver();
18455
18595
  const allReviews = [];
18456
18596
  let pagesFetched = 0;
18597
+ let stoppedEarly = null;
18457
18598
  try {
18458
18599
  await driver.launch({
18459
18600
  headless: false,
@@ -18471,6 +18612,12 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
18471
18612
  if (cards.length === 0) break;
18472
18613
  allReviews.push(...cards);
18473
18614
  }
18615
+ } catch (err) {
18616
+ if (allReviews.length === 0) throw err;
18617
+ stoppedEarly = {
18618
+ code: "review_scrape_stopped_early",
18619
+ message: err instanceof Error ? err.message : String(err)
18620
+ };
18474
18621
  } finally {
18475
18622
  await driver.close();
18476
18623
  }
@@ -18496,6 +18643,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
18496
18643
  pagesFetched,
18497
18644
  reviewCount: allReviews.length,
18498
18645
  reviews: allReviews,
18646
+ stoppedEarly,
18499
18647
  durationMs: Date.now() - startMs
18500
18648
  };
18501
18649
  await logRequestEvent({ userId: user.id, source: "trustpilot_reviews", status: "done", query: domain, resultCount: allReviews.length, result });
@@ -23981,6 +24129,13 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
23981
24129
  // src/api/stripe-routes.ts
23982
24130
  import Stripe from "stripe";
23983
24131
  import { Hono as Hono22 } from "hono";
24132
+
24133
+ // src/api/stripe-credit-policy.ts
24134
+ function shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoiceTotal) {
24135
+ return invoiceHasTierLine && (invoiceTotal ?? 0) > 0;
24136
+ }
24137
+
24138
+ // src/api/stripe-routes.ts
23984
24139
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
23985
24140
  async function cancelStandaloneMemorySub(memSubId, baseSubId) {
23986
24141
  if (!memSubId || memSubId === baseSubId) return;
@@ -24005,17 +24160,30 @@ stripeApp.post("/webhooks", async (c) => {
24005
24160
  } catch {
24006
24161
  return c.json({ error: "Invalid signature" }, 400);
24007
24162
  }
24008
- const isNew = await recordStripeEvent(event.id);
24009
- if (!isNew) return c.json({ received: true });
24163
+ const claim = await claimStripeEvent(event.id);
24164
+ if (claim.state !== "claimed") return c.json({ received: true });
24165
+ try {
24166
+ await processStripeEvent(event);
24167
+ const completed = await completeStripeEvent(event.id, claim.token);
24168
+ if (!completed) throw new Error("Stripe event claim was superseded before completion");
24169
+ return c.json({ received: true });
24170
+ } catch (error) {
24171
+ const message = error instanceof Error ? error.message : String(error);
24172
+ await failStripeEvent(event.id, claim.token, message).catch(() => false);
24173
+ console.error("[stripe] webhook processing failed", event.id, message);
24174
+ return c.json({ error: "Webhook processing failed" }, 500);
24175
+ }
24176
+ });
24177
+ async function processStripeEvent(event) {
24010
24178
  if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
24011
24179
  const invoice = event.data.object;
24012
24180
  const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
24013
- if (memLineId) return c.json({ received: true });
24181
+ if (memLineId) return;
24014
24182
  const invoiceHasTierLine = invoice.lines.data.some((l) => {
24015
24183
  const id = linePriceId(l);
24016
24184
  return id && id in SUBSCRIPTION_TIERS;
24017
24185
  });
24018
- if (invoiceHasTierLine) {
24186
+ if (shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoice.total)) {
24019
24187
  const subId = invoice.subscription;
24020
24188
  const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
24021
24189
  const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
@@ -24048,10 +24216,10 @@ stripeApp.post("/webhooks", async (c) => {
24048
24216
  await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
24049
24217
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
24050
24218
  }
24051
- return c.json({ received: true });
24219
+ return;
24052
24220
  }
24053
24221
  const user = await getUserByStripeCustomerId(sub.customer);
24054
- if (!user) return c.json({ received: true });
24222
+ if (!user) return;
24055
24223
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
24056
24224
  const tier = SUBSCRIPTION_TIERS[priceId];
24057
24225
  const live = sub.status === "active" || sub.status === "trialing";
@@ -24071,7 +24239,7 @@ stripeApp.post("/webhooks", async (c) => {
24071
24239
  if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
24072
24240
  }
24073
24241
  } else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
24074
- await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
24242
+ await setExtraConcurrencySlots(user.id, 1);
24075
24243
  await setConcurrencySubId(user.id, sub.id);
24076
24244
  }
24077
24245
  }
@@ -24088,7 +24256,7 @@ stripeApp.post("/webhooks", async (c) => {
24088
24256
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
24089
24257
  }
24090
24258
  }
24091
- return c.json({ received: true });
24259
+ return;
24092
24260
  }
24093
24261
  if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
24094
24262
  const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
@@ -24096,17 +24264,16 @@ stripeApp.post("/webhooks", async (c) => {
24096
24264
  await setMemoryPlan(user2.id, "free", null);
24097
24265
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
24098
24266
  }
24099
- return c.json({ received: true });
24267
+ return;
24100
24268
  }
24101
24269
  const user = await getUserByStripeCustomerId(sub.customer);
24102
- if (!user) return c.json({ received: true });
24270
+ if (!user) return;
24103
24271
  if (priceId === CONCURRENCY_PRICE_ID) {
24104
- await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
24272
+ await setExtraConcurrencySlots(user.id, 0);
24105
24273
  await setConcurrencySubId(user.id, null);
24106
24274
  }
24107
24275
  }
24108
- return c.json({ received: true });
24109
- });
24276
+ }
24110
24277
  function linePriceId(line) {
24111
24278
  const l = line;
24112
24279
  return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
@@ -30511,6 +30678,7 @@ function opForCostPath(p) {
30511
30678
  if (p.startsWith("/facebook/")) {
30512
30679
  if (p.endsWith("transcribe")) return "fb_transcribe";
30513
30680
  if (p.endsWith("/search")) return "fb_search";
30681
+ if (p.endsWith("/page-intel")) return "fb_page_intel";
30514
30682
  return "fb_ad";
30515
30683
  }
30516
30684
  if (p.startsWith("/instagram/")) return "instagram";
@@ -33177,7 +33345,7 @@ app.get("/cron/tick", async (c) => {
33177
33345
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
33178
33346
  return c.json({ error: "Unauthorized" }, 401);
33179
33347
  }
33180
- const { drainQueue } = await import("./worker-TTFXPPDK.js");
33348
+ const { drainQueue } = await import("./worker-XCPU4YSN.js");
33181
33349
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
33182
33350
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
33183
33351
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation] = await Promise.all([
@@ -33226,8 +33394,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
33226
33394
  finishExtractJob: finishExtractJob2,
33227
33395
  terminalExtractJobStatus: terminalExtractJobStatus2,
33228
33396
  extractJobLimitInfo: extractJobLimitInfo2
33229
- } = await import("./site-extract-repository-I3VM6WXN.js");
33230
- const { assembleExtractArtifacts } = await import("./extract-bundle-346R6MXD.js");
33397
+ } = await import("./site-extract-repository-XSPJTCIL.js");
33398
+ const { assembleExtractArtifacts } = await import("./extract-bundle-KUBX6N6Z.js");
33231
33399
  const existing = await getExtractJob2(jobId2);
33232
33400
  if (!existing) return c.json({ error: "job not found" }, 404);
33233
33401
  if (existing.status !== "failed") {
@@ -33254,7 +33422,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
33254
33422
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
33255
33423
  let settlement = "already_settled_or_refunded";
33256
33424
  if (job.billedMc == null && job.userId != null) {
33257
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-I3VM6WXN.js");
33425
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-XSPJTCIL.js");
33258
33426
  const heldMc = Number(job.options.heldMc ?? 0);
33259
33427
  const successful = await countSuccessfulPages2(jobId2);
33260
33428
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -33425,4 +33593,4 @@ app.get("/blog/:slug/", (c) => {
33425
33593
  export {
33426
33594
  app
33427
33595
  };
33428
- //# sourceMappingURL=server-3Q6XIJPF.js.map
33596
+ //# sourceMappingURL=server-5EX6XBIA.js.map