mcp-scraper 0.38.1 → 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 +718 -276
  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-C5UGV7RM.js → chunk-EGJKUB4Q.js} +3 -3
  20. package/dist/{chunk-C5UGV7RM.js.map → chunk-EGJKUB4Q.js.map} +1 -1
  21. package/dist/{chunk-WS2E7HA5.js → chunk-FRYT3ID4.js} +2 -2
  22. package/dist/{chunk-XKCZOPZN.js → chunk-N7KUTTCC.js} +235 -40
  23. package/dist/chunk-N7KUTTCC.js.map +1 -0
  24. package/dist/{chunk-BSD2Z5A6.js → chunk-NGM237OO.js} +66 -5
  25. package/dist/chunk-NGM237OO.js.map +1 -0
  26. package/dist/{chunk-YAW3AUTP.js → chunk-NKCCGADE.js} +23 -8
  27. package/dist/chunk-NKCCGADE.js.map +1 -0
  28. package/dist/{chunk-OLRRIA4H.js → chunk-SFRMFGQ6.js} +2 -2
  29. package/dist/{chunk-4YAFJWFG.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-JRFMBTRD.js → db-C5KVCOYT.js} +8 -4
  33. package/dist/{extract-bundle-DSFOHMAT.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-OBJYZEGE.js → location-data-repository-TTWF3OTM.js} +3 -3
  38. package/dist/{server-2UDB6PTC.js → server-5EX6XBIA.js} +312 -151
  39. package/dist/server-5EX6XBIA.js.map +1 -0
  40. package/dist/{site-extract-repository-2YJ33J5C.js → site-extract-repository-XSPJTCIL.js} +4 -4
  41. package/dist/{worker-ESF3HCNJ.js → worker-XCPU4YSN.js} +6 -6
  42. package/docs/mcp-tool-manifest.generated.json +66 -9
  43. package/package.json +1 -1
  44. package/dist/chunk-BSD2Z5A6.js.map +0 -1
  45. package/dist/chunk-FFBPWVWC.js +0 -7
  46. package/dist/chunk-FFBPWVWC.js.map +0 -1
  47. package/dist/chunk-PWPUKR5U.js.map +0 -1
  48. package/dist/chunk-XKCZOPZN.js.map +0 -1
  49. package/dist/chunk-YAW3AUTP.js.map +0 -1
  50. package/dist/server-2UDB6PTC.js.map +0 -1
  51. /package/dist/{chunk-WS2E7HA5.js.map → chunk-FRYT3ID4.js.map} +0 -0
  52. /package/dist/{chunk-OLRRIA4H.js.map → chunk-SFRMFGQ6.js.map} +0 -0
  53. /package/dist/{chunk-4YAFJWFG.js.map → chunk-YCI2PNCS.js.map} +0 -0
  54. /package/dist/{db-JRFMBTRD.js.map → db-C5KVCOYT.js.map} +0 -0
  55. /package/dist/{extract-bundle-DSFOHMAT.js.map → extract-bundle-KUBX6N6Z.js.map} +0 -0
  56. /package/dist/{location-data-repository-OBJYZEGE.js.map → location-data-repository-TTWF3OTM.js.map} +0 -0
  57. /package/dist/{site-extract-repository-2YJ33J5C.js.map → site-extract-repository-XSPJTCIL.js.map} +0 -0
  58. /package/dist/{worker-ESF3HCNJ.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-WS2E7HA5.js";
12
+ } from "./chunk-FRYT3ID4.js";
13
13
  import {
14
14
  classifyHarvestProblem,
15
15
  createHarvestAttemptRecorder,
16
16
  harvestProblemResponse,
17
17
  serializeHarvestProblem
18
- } from "./chunk-OLRRIA4H.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-BSD2Z5A6.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-YAW3AUTP.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-C5UGV7RM.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-FFBPWVWC.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-4YAFJWFG.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-XKCZOPZN.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-DSFOHMAT.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-OBJYZEGE.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-OBJYZEGE.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 });
@@ -24012,12 +24160,25 @@ stripeApp.post("/webhooks", async (c) => {
24012
24160
  } catch {
24013
24161
  return c.json({ error: "Invalid signature" }, 400);
24014
24162
  }
24015
- const isNew = await recordStripeEvent(event.id);
24016
- 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) {
24017
24178
  if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
24018
24179
  const invoice = event.data.object;
24019
24180
  const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
24020
- if (memLineId) return c.json({ received: true });
24181
+ if (memLineId) return;
24021
24182
  const invoiceHasTierLine = invoice.lines.data.some((l) => {
24022
24183
  const id = linePriceId(l);
24023
24184
  return id && id in SUBSCRIPTION_TIERS;
@@ -24055,10 +24216,10 @@ stripeApp.post("/webhooks", async (c) => {
24055
24216
  await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
24056
24217
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
24057
24218
  }
24058
- return c.json({ received: true });
24219
+ return;
24059
24220
  }
24060
24221
  const user = await getUserByStripeCustomerId(sub.customer);
24061
- if (!user) return c.json({ received: true });
24222
+ if (!user) return;
24062
24223
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
24063
24224
  const tier = SUBSCRIPTION_TIERS[priceId];
24064
24225
  const live = sub.status === "active" || sub.status === "trialing";
@@ -24078,7 +24239,7 @@ stripeApp.post("/webhooks", async (c) => {
24078
24239
  if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
24079
24240
  }
24080
24241
  } else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
24081
- await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
24242
+ await setExtraConcurrencySlots(user.id, 1);
24082
24243
  await setConcurrencySubId(user.id, sub.id);
24083
24244
  }
24084
24245
  }
@@ -24095,7 +24256,7 @@ stripeApp.post("/webhooks", async (c) => {
24095
24256
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
24096
24257
  }
24097
24258
  }
24098
- return c.json({ received: true });
24259
+ return;
24099
24260
  }
24100
24261
  if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
24101
24262
  const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
@@ -24103,17 +24264,16 @@ stripeApp.post("/webhooks", async (c) => {
24103
24264
  await setMemoryPlan(user2.id, "free", null);
24104
24265
  await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
24105
24266
  }
24106
- return c.json({ received: true });
24267
+ return;
24107
24268
  }
24108
24269
  const user = await getUserByStripeCustomerId(sub.customer);
24109
- if (!user) return c.json({ received: true });
24270
+ if (!user) return;
24110
24271
  if (priceId === CONCURRENCY_PRICE_ID) {
24111
- await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
24272
+ await setExtraConcurrencySlots(user.id, 0);
24112
24273
  await setConcurrencySubId(user.id, null);
24113
24274
  }
24114
24275
  }
24115
- return c.json({ received: true });
24116
- });
24276
+ }
24117
24277
  function linePriceId(line) {
24118
24278
  const l = line;
24119
24279
  return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
@@ -30518,6 +30678,7 @@ function opForCostPath(p) {
30518
30678
  if (p.startsWith("/facebook/")) {
30519
30679
  if (p.endsWith("transcribe")) return "fb_transcribe";
30520
30680
  if (p.endsWith("/search")) return "fb_search";
30681
+ if (p.endsWith("/page-intel")) return "fb_page_intel";
30521
30682
  return "fb_ad";
30522
30683
  }
30523
30684
  if (p.startsWith("/instagram/")) return "instagram";
@@ -33184,7 +33345,7 @@ app.get("/cron/tick", async (c) => {
33184
33345
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
33185
33346
  return c.json({ error: "Unauthorized" }, 401);
33186
33347
  }
33187
- const { drainQueue } = await import("./worker-ESF3HCNJ.js");
33348
+ const { drainQueue } = await import("./worker-XCPU4YSN.js");
33188
33349
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
33189
33350
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
33190
33351
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation] = await Promise.all([
@@ -33233,8 +33394,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
33233
33394
  finishExtractJob: finishExtractJob2,
33234
33395
  terminalExtractJobStatus: terminalExtractJobStatus2,
33235
33396
  extractJobLimitInfo: extractJobLimitInfo2
33236
- } = await import("./site-extract-repository-2YJ33J5C.js");
33237
- const { assembleExtractArtifacts } = await import("./extract-bundle-DSFOHMAT.js");
33397
+ } = await import("./site-extract-repository-XSPJTCIL.js");
33398
+ const { assembleExtractArtifacts } = await import("./extract-bundle-KUBX6N6Z.js");
33238
33399
  const existing = await getExtractJob2(jobId2);
33239
33400
  if (!existing) return c.json({ error: "job not found" }, 404);
33240
33401
  if (existing.status !== "failed") {
@@ -33261,7 +33422,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
33261
33422
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
33262
33423
  let settlement = "already_settled_or_refunded";
33263
33424
  if (job.billedMc == null && job.userId != null) {
33264
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-2YJ33J5C.js");
33425
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-XSPJTCIL.js");
33265
33426
  const heldMc = Number(job.options.heldMc ?? 0);
33266
33427
  const successful = await countSuccessfulPages2(jobId2);
33267
33428
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -33432,4 +33593,4 @@ app.get("/blog/:slug/", (c) => {
33432
33593
  export {
33433
33594
  app
33434
33595
  };
33435
- //# sourceMappingURL=server-2UDB6PTC.js.map
33596
+ //# sourceMappingURL=server-5EX6XBIA.js.map