@iflow-mcp/jakeliume-webpeel 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +313 -0
- package/dist/cache.d.ts +30 -0
- package/dist/cache.js +139 -0
- package/dist/cli/commands/auth.d.ts +5 -0
- package/dist/cli/commands/auth.js +411 -0
- package/dist/cli/commands/doctor.d.ts +37 -0
- package/dist/cli/commands/doctor.js +371 -0
- package/dist/cli/commands/fetch.d.ts +6 -0
- package/dist/cli/commands/fetch.js +1345 -0
- package/dist/cli/commands/guide.d.ts +2 -0
- package/dist/cli/commands/guide.js +183 -0
- package/dist/cli/commands/interact.d.ts +5 -0
- package/dist/cli/commands/interact.js +840 -0
- package/dist/cli/commands/jobs.d.ts +5 -0
- package/dist/cli/commands/jobs.js +997 -0
- package/dist/cli/commands/monitor.d.ts +12 -0
- package/dist/cli/commands/monitor.js +197 -0
- package/dist/cli/commands/observe.d.ts +12 -0
- package/dist/cli/commands/observe.js +158 -0
- package/dist/cli/commands/screenshot.d.ts +5 -0
- package/dist/cli/commands/screenshot.js +282 -0
- package/dist/cli/commands/search.d.ts +5 -0
- package/dist/cli/commands/search.js +1021 -0
- package/dist/cli/commands/setup.d.ts +13 -0
- package/dist/cli/commands/setup.js +244 -0
- package/dist/cli/commands/skill.d.ts +15 -0
- package/dist/cli/commands/skill.js +195 -0
- package/dist/cli/utils.d.ts +84 -0
- package/dist/cli/utils.js +806 -0
- package/dist/cli-auth.d.ts +75 -0
- package/dist/cli-auth.js +369 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +99 -0
- package/dist/core/actions.d.ts +69 -0
- package/dist/core/actions.js +495 -0
- package/dist/core/agent.d.ts +98 -0
- package/dist/core/agent.js +558 -0
- package/dist/core/answer.d.ts +42 -0
- package/dist/core/answer.js +395 -0
- package/dist/core/application-tracker.d.ts +84 -0
- package/dist/core/application-tracker.js +184 -0
- package/dist/core/apply.d.ts +162 -0
- package/dist/core/apply.js +816 -0
- package/dist/core/auth-detection.d.ts +35 -0
- package/dist/core/auth-detection.js +358 -0
- package/dist/core/auto-extract.d.ts +82 -0
- package/dist/core/auto-extract.js +604 -0
- package/dist/core/auto-interact.d.ts +23 -0
- package/dist/core/auto-interact.js +246 -0
- package/dist/core/bm25-filter.d.ts +66 -0
- package/dist/core/bm25-filter.js +288 -0
- package/dist/core/branding.d.ts +54 -0
- package/dist/core/branding.js +234 -0
- package/dist/core/browser-fetch.d.ts +323 -0
- package/dist/core/browser-fetch.js +1600 -0
- package/dist/core/browser-pool.d.ts +91 -0
- package/dist/core/browser-pool.js +550 -0
- package/dist/core/budget.d.ts +42 -0
- package/dist/core/budget.js +324 -0
- package/dist/core/business-intel.d.ts +47 -0
- package/dist/core/business-intel.js +279 -0
- package/dist/core/cache.d.ts +13 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cf-worker-proxy.d.ts +32 -0
- package/dist/core/cf-worker-proxy.js +87 -0
- package/dist/core/challenge-detection.d.ts +26 -0
- package/dist/core/challenge-detection.js +468 -0
- package/dist/core/change-tracking.d.ts +75 -0
- package/dist/core/change-tracking.js +276 -0
- package/dist/core/chunker.d.ts +46 -0
- package/dist/core/chunker.js +249 -0
- package/dist/core/chunking.d.ts +42 -0
- package/dist/core/chunking.js +181 -0
- package/dist/core/circuit-breaker.d.ts +44 -0
- package/dist/core/circuit-breaker.js +85 -0
- package/dist/core/content-pruner.d.ts +47 -0
- package/dist/core/content-pruner.js +425 -0
- package/dist/core/cookie-cache.d.ts +60 -0
- package/dist/core/cookie-cache.js +163 -0
- package/dist/core/crawl-checkpoint.d.ts +54 -0
- package/dist/core/crawl-checkpoint.js +104 -0
- package/dist/core/crawler.d.ts +84 -0
- package/dist/core/crawler.js +349 -0
- package/dist/core/cross-verify.d.ts +27 -0
- package/dist/core/cross-verify.js +93 -0
- package/dist/core/deep-fetch.d.ts +74 -0
- package/dist/core/deep-fetch.js +405 -0
- package/dist/core/deep-research.d.ts +141 -0
- package/dist/core/deep-research.js +972 -0
- package/dist/core/design-analysis.d.ts +70 -0
- package/dist/core/design-analysis.js +490 -0
- package/dist/core/design-compare.d.ts +38 -0
- package/dist/core/design-compare.js +264 -0
- package/dist/core/diff.d.ts +61 -0
- package/dist/core/diff.js +289 -0
- package/dist/core/dns-cache.d.ts +20 -0
- package/dist/core/dns-cache.js +198 -0
- package/dist/core/documents.d.ts +23 -0
- package/dist/core/documents.js +123 -0
- package/dist/core/domain-memory.d.ts +66 -0
- package/dist/core/domain-memory.js +163 -0
- package/dist/core/domain-verify.d.ts +40 -0
- package/dist/core/domain-verify.js +379 -0
- package/dist/core/engine-ranker.d.ts +112 -0
- package/dist/core/engine-ranker.js +395 -0
- package/dist/core/extract-inline.d.ts +38 -0
- package/dist/core/extract-inline.js +215 -0
- package/dist/core/extract-listings.d.ts +38 -0
- package/dist/core/extract-listings.js +461 -0
- package/dist/core/extract.d.ts +9 -0
- package/dist/core/extract.js +139 -0
- package/dist/core/fetch-cache.d.ts +57 -0
- package/dist/core/fetch-cache.js +95 -0
- package/dist/core/fetcher.d.ts +13 -0
- package/dist/core/fetcher.js +12 -0
- package/dist/core/google-cache.d.ts +29 -0
- package/dist/core/google-cache.js +180 -0
- package/dist/core/google-serp-parser.d.ts +82 -0
- package/dist/core/google-serp-parser.js +287 -0
- package/dist/core/hotel-search.d.ts +122 -0
- package/dist/core/hotel-search.js +382 -0
- package/dist/core/http-fetch.d.ts +72 -0
- package/dist/core/http-fetch.js +820 -0
- package/dist/core/human.d.ts +175 -0
- package/dist/core/human.js +680 -0
- package/dist/core/image-caption.d.ts +44 -0
- package/dist/core/image-caption.js +271 -0
- package/dist/core/jobs.d.ts +75 -0
- package/dist/core/jobs.js +634 -0
- package/dist/core/json-ld.d.ts +15 -0
- package/dist/core/json-ld.js +617 -0
- package/dist/core/language-detect.d.ts +18 -0
- package/dist/core/language-detect.js +135 -0
- package/dist/core/links.d.ts +10 -0
- package/dist/core/links.js +44 -0
- package/dist/core/llm-extract.d.ts +71 -0
- package/dist/core/llm-extract.js +507 -0
- package/dist/core/llm-provider.d.ts +100 -0
- package/dist/core/llm-provider.js +702 -0
- package/dist/core/local-search.d.ts +60 -0
- package/dist/core/local-search.js +308 -0
- package/dist/core/logger.d.ts +28 -0
- package/dist/core/logger.js +104 -0
- package/dist/core/map.d.ts +33 -0
- package/dist/core/map.js +127 -0
- package/dist/core/markdown.d.ts +92 -0
- package/dist/core/markdown.js +809 -0
- package/dist/core/metadata.d.ts +34 -0
- package/dist/core/metadata.js +422 -0
- package/dist/core/observe.d.ts +113 -0
- package/dist/core/observe.js +395 -0
- package/dist/core/ocr.d.ts +12 -0
- package/dist/core/ocr.js +33 -0
- package/dist/core/paginate.d.ts +31 -0
- package/dist/core/paginate.js +106 -0
- package/dist/core/pdf.d.ts +8 -0
- package/dist/core/pdf.js +25 -0
- package/dist/core/peel-tls.d.ts +25 -0
- package/dist/core/peel-tls.js +220 -0
- package/dist/core/pipeline.d.ts +132 -0
- package/dist/core/pipeline.js +1666 -0
- package/dist/core/profiles.d.ts +61 -0
- package/dist/core/profiles.js +350 -0
- package/dist/core/prompt-guard.d.ts +30 -0
- package/dist/core/prompt-guard.js +119 -0
- package/dist/core/proxy-config.d.ts +90 -0
- package/dist/core/proxy-config.js +172 -0
- package/dist/core/quick-answer.d.ts +53 -0
- package/dist/core/quick-answer.js +833 -0
- package/dist/core/rate-governor.d.ts +80 -0
- package/dist/core/rate-governor.js +238 -0
- package/dist/core/readability.d.ts +57 -0
- package/dist/core/readability.js +533 -0
- package/dist/core/research.d.ts +66 -0
- package/dist/core/research.js +270 -0
- package/dist/core/retry.d.ts +60 -0
- package/dist/core/retry.js +119 -0
- package/dist/core/safe-browsing.d.ts +30 -0
- package/dist/core/safe-browsing.js +206 -0
- package/dist/core/schema-extraction.d.ts +66 -0
- package/dist/core/schema-extraction.js +352 -0
- package/dist/core/schema-postprocess.d.ts +32 -0
- package/dist/core/schema-postprocess.js +469 -0
- package/dist/core/schema-templates.d.ts +19 -0
- package/dist/core/schema-templates.js +143 -0
- package/dist/core/screenshot.d.ts +224 -0
- package/dist/core/screenshot.js +207 -0
- package/dist/core/search-engines.d.ts +25 -0
- package/dist/core/search-engines.js +182 -0
- package/dist/core/search-provider.d.ts +243 -0
- package/dist/core/search-provider.js +1629 -0
- package/dist/core/searxng-provider.d.ts +35 -0
- package/dist/core/searxng-provider.js +105 -0
- package/dist/core/selective-evidence.d.ts +151 -0
- package/dist/core/selective-evidence.js +389 -0
- package/dist/core/site-search.d.ts +44 -0
- package/dist/core/site-search.js +252 -0
- package/dist/core/sitemap.d.ts +23 -0
- package/dist/core/sitemap.js +105 -0
- package/dist/core/source-credibility.d.ts +29 -0
- package/dist/core/source-credibility.js +584 -0
- package/dist/core/source-scoring.d.ts +166 -0
- package/dist/core/source-scoring.js +396 -0
- package/dist/core/stemmer.d.ts +38 -0
- package/dist/core/stemmer.js +509 -0
- package/dist/core/strategies.d.ts +104 -0
- package/dist/core/strategies.js +1044 -0
- package/dist/core/strategy-hooks.d.ts +145 -0
- package/dist/core/strategy-hooks.js +74 -0
- package/dist/core/structured-extract.d.ts +43 -0
- package/dist/core/structured-extract.js +550 -0
- package/dist/core/summarize.d.ts +17 -0
- package/dist/core/summarize.js +78 -0
- package/dist/core/synonyms.d.ts +42 -0
- package/dist/core/synonyms.js +184 -0
- package/dist/core/system-monitor.d.ts +61 -0
- package/dist/core/system-monitor.js +133 -0
- package/dist/core/table-format.d.ts +30 -0
- package/dist/core/table-format.js +146 -0
- package/dist/core/threat-feeds.d.ts +23 -0
- package/dist/core/threat-feeds.js +104 -0
- package/dist/core/timing.d.ts +21 -0
- package/dist/core/timing.js +33 -0
- package/dist/core/transcript-export.d.ts +47 -0
- package/dist/core/transcript-export.js +107 -0
- package/dist/core/user-agents.d.ts +82 -0
- package/dist/core/user-agents.js +239 -0
- package/dist/core/vertical-search.d.ts +54 -0
- package/dist/core/vertical-search.js +158 -0
- package/dist/core/watch-manager.d.ts +175 -0
- package/dist/core/watch-manager.js +416 -0
- package/dist/core/watch.d.ts +101 -0
- package/dist/core/watch.js +389 -0
- package/dist/core/youtube.d.ts +130 -0
- package/dist/core/youtube.js +1175 -0
- package/dist/ee/challenge-re-export.d.ts +1 -0
- package/dist/ee/challenge-re-export.js +1 -0
- package/dist/ee/challenge-solver.d.ts +72 -0
- package/dist/ee/challenge-solver.js +720 -0
- package/dist/ee/domain-extractors.d.ts +8 -0
- package/dist/ee/domain-extractors.js +8 -0
- package/dist/ee/domain-intel.d.ts +16 -0
- package/dist/ee/domain-intel.js +133 -0
- package/dist/ee/extractors/allrecipes.d.ts +2 -0
- package/dist/ee/extractors/allrecipes.js +120 -0
- package/dist/ee/extractors/amazon.d.ts +2 -0
- package/dist/ee/extractors/amazon.js +78 -0
- package/dist/ee/extractors/arxiv.d.ts +2 -0
- package/dist/ee/extractors/arxiv.js +137 -0
- package/dist/ee/extractors/bestbuy.d.ts +2 -0
- package/dist/ee/extractors/bestbuy.js +78 -0
- package/dist/ee/extractors/carscom.d.ts +2 -0
- package/dist/ee/extractors/carscom.js +121 -0
- package/dist/ee/extractors/coingecko.d.ts +2 -0
- package/dist/ee/extractors/coingecko.js +134 -0
- package/dist/ee/extractors/craigslist.d.ts +2 -0
- package/dist/ee/extractors/craigslist.js +92 -0
- package/dist/ee/extractors/devto.d.ts +2 -0
- package/dist/ee/extractors/devto.js +135 -0
- package/dist/ee/extractors/ebay.d.ts +2 -0
- package/dist/ee/extractors/ebay.js +90 -0
- package/dist/ee/extractors/espn.d.ts +2 -0
- package/dist/ee/extractors/espn.js +260 -0
- package/dist/ee/extractors/etsy.d.ts +2 -0
- package/dist/ee/extractors/etsy.js +52 -0
- package/dist/ee/extractors/facebook.d.ts +2 -0
- package/dist/ee/extractors/facebook.js +46 -0
- package/dist/ee/extractors/github.d.ts +2 -0
- package/dist/ee/extractors/github.js +196 -0
- package/dist/ee/extractors/google-flights.d.ts +2 -0
- package/dist/ee/extractors/google-flights.js +176 -0
- package/dist/ee/extractors/hackernews.d.ts +2 -0
- package/dist/ee/extractors/hackernews.js +147 -0
- package/dist/ee/extractors/imdb.d.ts +2 -0
- package/dist/ee/extractors/imdb.js +172 -0
- package/dist/ee/extractors/index.d.ts +26 -0
- package/dist/ee/extractors/index.js +247 -0
- package/dist/ee/extractors/instagram.d.ts +2 -0
- package/dist/ee/extractors/instagram.js +102 -0
- package/dist/ee/extractors/kalshi.d.ts +2 -0
- package/dist/ee/extractors/kalshi.js +121 -0
- package/dist/ee/extractors/kayak-cars.d.ts +2 -0
- package/dist/ee/extractors/kayak-cars.js +270 -0
- package/dist/ee/extractors/linkedin.d.ts +2 -0
- package/dist/ee/extractors/linkedin.js +113 -0
- package/dist/ee/extractors/medium.d.ts +2 -0
- package/dist/ee/extractors/medium.js +130 -0
- package/dist/ee/extractors/news.d.ts +4 -0
- package/dist/ee/extractors/news.js +173 -0
- package/dist/ee/extractors/npm.d.ts +2 -0
- package/dist/ee/extractors/npm.js +86 -0
- package/dist/ee/extractors/pdf.d.ts +2 -0
- package/dist/ee/extractors/pdf.js +108 -0
- package/dist/ee/extractors/pinterest.d.ts +2 -0
- package/dist/ee/extractors/pinterest.js +34 -0
- package/dist/ee/extractors/polymarket.d.ts +2 -0
- package/dist/ee/extractors/polymarket.js +358 -0
- package/dist/ee/extractors/producthunt.d.ts +2 -0
- package/dist/ee/extractors/producthunt.js +88 -0
- package/dist/ee/extractors/pubmed.d.ts +2 -0
- package/dist/ee/extractors/pubmed.js +162 -0
- package/dist/ee/extractors/pypi.d.ts +2 -0
- package/dist/ee/extractors/pypi.js +80 -0
- package/dist/ee/extractors/reddit.d.ts +2 -0
- package/dist/ee/extractors/reddit.js +438 -0
- package/dist/ee/extractors/redfin.d.ts +2 -0
- package/dist/ee/extractors/redfin.js +156 -0
- package/dist/ee/extractors/semanticscholar.d.ts +2 -0
- package/dist/ee/extractors/semanticscholar.js +131 -0
- package/dist/ee/extractors/shared.d.ts +12 -0
- package/dist/ee/extractors/shared.js +76 -0
- package/dist/ee/extractors/soundcloud.d.ts +2 -0
- package/dist/ee/extractors/soundcloud.js +34 -0
- package/dist/ee/extractors/sportsbetting.d.ts +2 -0
- package/dist/ee/extractors/sportsbetting.js +37 -0
- package/dist/ee/extractors/spotify.d.ts +2 -0
- package/dist/ee/extractors/spotify.js +34 -0
- package/dist/ee/extractors/stackoverflow.d.ts +2 -0
- package/dist/ee/extractors/stackoverflow.js +61 -0
- package/dist/ee/extractors/substack.d.ts +2 -0
- package/dist/ee/extractors/substack.js +115 -0
- package/dist/ee/extractors/substackroot.d.ts +2 -0
- package/dist/ee/extractors/substackroot.js +46 -0
- package/dist/ee/extractors/tiktok.d.ts +2 -0
- package/dist/ee/extractors/tiktok.js +29 -0
- package/dist/ee/extractors/tradingview.d.ts +2 -0
- package/dist/ee/extractors/tradingview.js +182 -0
- package/dist/ee/extractors/twitch.d.ts +2 -0
- package/dist/ee/extractors/twitch.js +36 -0
- package/dist/ee/extractors/twitter.d.ts +2 -0
- package/dist/ee/extractors/twitter.js +327 -0
- package/dist/ee/extractors/types.d.ts +14 -0
- package/dist/ee/extractors/types.js +1 -0
- package/dist/ee/extractors/walmart.d.ts +2 -0
- package/dist/ee/extractors/walmart.js +50 -0
- package/dist/ee/extractors/weather.d.ts +2 -0
- package/dist/ee/extractors/weather.js +133 -0
- package/dist/ee/extractors/wikipedia.d.ts +4 -0
- package/dist/ee/extractors/wikipedia.js +235 -0
- package/dist/ee/extractors/yelp.d.ts +2 -0
- package/dist/ee/extractors/yelp.js +216 -0
- package/dist/ee/extractors/youtube.d.ts +2 -0
- package/dist/ee/extractors/youtube.js +189 -0
- package/dist/ee/extractors/zillow.d.ts +54 -0
- package/dist/ee/extractors/zillow.js +247 -0
- package/dist/ee/extractors-re-export.d.ts +1 -0
- package/dist/ee/extractors-re-export.js +1 -0
- package/dist/ee/premium-hooks.d.ts +20 -0
- package/dist/ee/premium-hooks.js +50 -0
- package/dist/ee/spa-detection.d.ts +2 -0
- package/dist/ee/spa-detection.js +2 -0
- package/dist/ee/stability.d.ts +4 -0
- package/dist/ee/stability.js +29 -0
- package/dist/ee/swr-cache.d.ts +14 -0
- package/dist/ee/swr-cache.js +34 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +291 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/langchain.d.ts +64 -0
- package/dist/integrations/langchain.js +115 -0
- package/dist/integrations/llamaindex.d.ts +50 -0
- package/dist/integrations/llamaindex.js +91 -0
- package/dist/mcp/handlers/act.d.ts +5 -0
- package/dist/mcp/handlers/act.js +34 -0
- package/dist/mcp/handlers/definitions.d.ts +6 -0
- package/dist/mcp/handlers/definitions.js +395 -0
- package/dist/mcp/handlers/extract.d.ts +7 -0
- package/dist/mcp/handlers/extract.js +135 -0
- package/dist/mcp/handlers/fetch.d.ts +6 -0
- package/dist/mcp/handlers/fetch.js +98 -0
- package/dist/mcp/handlers/find.d.ts +5 -0
- package/dist/mcp/handlers/find.js +137 -0
- package/dist/mcp/handlers/index.d.ts +13 -0
- package/dist/mcp/handlers/index.js +63 -0
- package/dist/mcp/handlers/legacy.d.ts +25 -0
- package/dist/mcp/handlers/legacy.js +450 -0
- package/dist/mcp/handlers/meta.d.ts +6 -0
- package/dist/mcp/handlers/meta.js +40 -0
- package/dist/mcp/handlers/monitor.d.ts +5 -0
- package/dist/mcp/handlers/monitor.js +41 -0
- package/dist/mcp/handlers/observe.d.ts +8 -0
- package/dist/mcp/handlers/observe.js +37 -0
- package/dist/mcp/handlers/read.d.ts +6 -0
- package/dist/mcp/handlers/read.js +78 -0
- package/dist/mcp/handlers/see.d.ts +5 -0
- package/dist/mcp/handlers/see.js +75 -0
- package/dist/mcp/handlers/types.d.ts +29 -0
- package/dist/mcp/handlers/types.js +28 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +108 -0
- package/dist/mcp/smart-router.d.ts +23 -0
- package/dist/mcp/smart-router.js +178 -0
- package/dist/server/app.d.ts +14 -0
- package/dist/server/app.js +632 -0
- package/dist/server/auth-store.d.ts +28 -0
- package/dist/server/auth-store.js +88 -0
- package/dist/server/bull-queues.d.ts +60 -0
- package/dist/server/bull-queues.js +90 -0
- package/dist/server/email-service.d.ts +55 -0
- package/dist/server/email-service.js +291 -0
- package/dist/server/job-queue.d.ts +100 -0
- package/dist/server/job-queue.js +145 -0
- package/dist/server/logger.d.ts +10 -0
- package/dist/server/logger.js +37 -0
- package/dist/server/middleware/audit-log.d.ts +14 -0
- package/dist/server/middleware/audit-log.js +73 -0
- package/dist/server/middleware/auth.d.ts +35 -0
- package/dist/server/middleware/auth.js +225 -0
- package/dist/server/middleware/rate-limit.d.ts +50 -0
- package/dist/server/middleware/rate-limit.js +270 -0
- package/dist/server/middleware/scope-guard.d.ts +25 -0
- package/dist/server/middleware/scope-guard.js +45 -0
- package/dist/server/middleware/url-validator.d.ts +15 -0
- package/dist/server/middleware/url-validator.js +201 -0
- package/dist/server/openapi.yaml +6418 -0
- package/dist/server/pg-auth-store.d.ts +146 -0
- package/dist/server/pg-auth-store.js +576 -0
- package/dist/server/pg-job-queue.d.ts +59 -0
- package/dist/server/pg-job-queue.js +375 -0
- package/dist/server/routes/activity.d.ts +6 -0
- package/dist/server/routes/activity.js +79 -0
- package/dist/server/routes/admin-active.d.ts +7 -0
- package/dist/server/routes/admin-active.js +120 -0
- package/dist/server/routes/admin-stats.d.ts +7 -0
- package/dist/server/routes/admin-stats.js +176 -0
- package/dist/server/routes/agent.d.ts +24 -0
- package/dist/server/routes/agent.js +480 -0
- package/dist/server/routes/answer.d.ts +5 -0
- package/dist/server/routes/answer.js +125 -0
- package/dist/server/routes/ask.d.ts +28 -0
- package/dist/server/routes/ask.js +295 -0
- package/dist/server/routes/batch.d.ts +6 -0
- package/dist/server/routes/batch.js +493 -0
- package/dist/server/routes/cache-warm.d.ts +25 -0
- package/dist/server/routes/cache-warm.js +212 -0
- package/dist/server/routes/cli-usage.d.ts +6 -0
- package/dist/server/routes/cli-usage.js +127 -0
- package/dist/server/routes/compat.d.ts +23 -0
- package/dist/server/routes/compat.js +652 -0
- package/dist/server/routes/crawl.d.ts +13 -0
- package/dist/server/routes/crawl.js +287 -0
- package/dist/server/routes/deep-fetch.d.ts +8 -0
- package/dist/server/routes/deep-fetch.js +57 -0
- package/dist/server/routes/deep-research.d.ts +11 -0
- package/dist/server/routes/deep-research.js +232 -0
- package/dist/server/routes/demo.d.ts +24 -0
- package/dist/server/routes/demo.js +517 -0
- package/dist/server/routes/do.d.ts +8 -0
- package/dist/server/routes/do.js +72 -0
- package/dist/server/routes/extract.d.ts +14 -0
- package/dist/server/routes/extract.js +325 -0
- package/dist/server/routes/feed.d.ts +15 -0
- package/dist/server/routes/feed.js +311 -0
- package/dist/server/routes/fetch-queue.d.ts +13 -0
- package/dist/server/routes/fetch-queue.js +357 -0
- package/dist/server/routes/fetch.d.ts +7 -0
- package/dist/server/routes/fetch.js +1274 -0
- package/dist/server/routes/go.d.ts +14 -0
- package/dist/server/routes/go.js +81 -0
- package/dist/server/routes/health.d.ts +11 -0
- package/dist/server/routes/health.js +141 -0
- package/dist/server/routes/jobs.d.ts +7 -0
- package/dist/server/routes/jobs.js +574 -0
- package/dist/server/routes/map.d.ts +11 -0
- package/dist/server/routes/map.js +116 -0
- package/dist/server/routes/mcp.d.ts +14 -0
- package/dist/server/routes/mcp.js +197 -0
- package/dist/server/routes/metrics.d.ts +37 -0
- package/dist/server/routes/metrics.js +149 -0
- package/dist/server/routes/oauth.d.ts +9 -0
- package/dist/server/routes/oauth.js +396 -0
- package/dist/server/routes/playground.d.ts +17 -0
- package/dist/server/routes/playground.js +283 -0
- package/dist/server/routes/reader.d.ts +18 -0
- package/dist/server/routes/reader.js +192 -0
- package/dist/server/routes/research.d.ts +14 -0
- package/dist/server/routes/research.js +482 -0
- package/dist/server/routes/screenshot.d.ts +22 -0
- package/dist/server/routes/screenshot.js +820 -0
- package/dist/server/routes/search.d.ts +6 -0
- package/dist/server/routes/search.js +874 -0
- package/dist/server/routes/session.d.ts +17 -0
- package/dist/server/routes/session.js +548 -0
- package/dist/server/routes/share.d.ts +18 -0
- package/dist/server/routes/share.js +462 -0
- package/dist/server/routes/smart-search/handlers/cars.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/cars.js +102 -0
- package/dist/server/routes/smart-search/handlers/flights.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/flights.js +72 -0
- package/dist/server/routes/smart-search/handlers/general.d.ts +13 -0
- package/dist/server/routes/smart-search/handlers/general.js +717 -0
- package/dist/server/routes/smart-search/handlers/hotels.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/hotels.js +88 -0
- package/dist/server/routes/smart-search/handlers/products.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/products.js +1309 -0
- package/dist/server/routes/smart-search/handlers/rental.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/rental.js +154 -0
- package/dist/server/routes/smart-search/handlers/restaurants.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/restaurants.js +225 -0
- package/dist/server/routes/smart-search/handlers/transit-verdict.d.ts +41 -0
- package/dist/server/routes/smart-search/handlers/transit-verdict.js +224 -0
- package/dist/server/routes/smart-search/index.d.ts +19 -0
- package/dist/server/routes/smart-search/index.js +546 -0
- package/dist/server/routes/smart-search/intent.d.ts +3 -0
- package/dist/server/routes/smart-search/intent.js +264 -0
- package/dist/server/routes/smart-search/llm.d.ts +16 -0
- package/dist/server/routes/smart-search/llm.js +70 -0
- package/dist/server/routes/smart-search/sources/reddit.d.ts +18 -0
- package/dist/server/routes/smart-search/sources/reddit.js +34 -0
- package/dist/server/routes/smart-search/sources/yelp.d.ts +25 -0
- package/dist/server/routes/smart-search/sources/yelp.js +171 -0
- package/dist/server/routes/smart-search/sources/youtube.d.ts +8 -0
- package/dist/server/routes/smart-search/sources/youtube.js +9 -0
- package/dist/server/routes/smart-search/types.d.ts +81 -0
- package/dist/server/routes/smart-search/types.js +1 -0
- package/dist/server/routes/smart-search/utils.d.ts +20 -0
- package/dist/server/routes/smart-search/utils.js +146 -0
- package/dist/server/routes/stats.d.ts +6 -0
- package/dist/server/routes/stats.js +71 -0
- package/dist/server/routes/stripe.d.ts +15 -0
- package/dist/server/routes/stripe.js +296 -0
- package/dist/server/routes/transcript-export.d.ts +10 -0
- package/dist/server/routes/transcript-export.js +178 -0
- package/dist/server/routes/usage.d.ts +9 -0
- package/dist/server/routes/usage.js +279 -0
- package/dist/server/routes/users.d.ts +8 -0
- package/dist/server/routes/users.js +1867 -0
- package/dist/server/routes/watch.d.ts +15 -0
- package/dist/server/routes/watch.js +309 -0
- package/dist/server/routes/webhooks.d.ts +26 -0
- package/dist/server/routes/webhooks.js +170 -0
- package/dist/server/routes/youtube.d.ts +6 -0
- package/dist/server/routes/youtube.js +130 -0
- package/dist/server/sentry.d.ts +14 -0
- package/dist/server/sentry.js +104 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.js +7 -0
- package/dist/server/utils/response.d.ts +44 -0
- package/dist/server/utils/response.js +69 -0
- package/dist/server/utils/sse.d.ts +22 -0
- package/dist/server/utils/sse.js +38 -0
- package/dist/types.d.ts +552 -0
- package/dist/types.js +39 -0
- package/llms.txt +105 -0
- package/package.json +189 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search endpoint with caching — supports DuckDuckGo (default) and Brave (BYOK)
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { fetch as undiciFetch } from 'undici';
|
|
6
|
+
import { load } from 'cheerio';
|
|
7
|
+
import { LRUCache } from 'lru-cache';
|
|
8
|
+
// @ts-ignore — ioredis CJS/ESM interop
|
|
9
|
+
import IoRedisModule from 'ioredis';
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const IoRedis = IoRedisModule.default ?? IoRedisModule;
|
|
12
|
+
import { peel } from '../../index.js';
|
|
13
|
+
import { simpleFetch } from '../../core/fetcher.js';
|
|
14
|
+
import { searchCache } from '../../core/fetch-cache.js';
|
|
15
|
+
import { getSearchProvider, getBestSearchProvider, } from '../../core/search-provider.js';
|
|
16
|
+
import { BaiduSearchProvider, YandexSearchProvider, NaverSearchProvider, YahooJapanSearchProvider } from '../../core/search-engines.js';
|
|
17
|
+
import { crossVerifySearch } from '../../core/cross-verify.js';
|
|
18
|
+
import { searchShopping, searchNews as searchNewsVertical, searchImages as searchImagesVertical, searchVideos, } from '../../core/vertical-search.js';
|
|
19
|
+
import { getSourceCredibility } from '../../core/source-credibility.js';
|
|
20
|
+
import { checkAndSendDualAlert } from '../email-service.js';
|
|
21
|
+
import { localSearch } from '../../core/local-search.js';
|
|
22
|
+
// ─── Redis client (lazy singleton for search instant cache) ───────────────
|
|
23
|
+
function buildSearchRedis() {
|
|
24
|
+
const url = process.env.REDIS_URL || 'redis://redis:6379';
|
|
25
|
+
const password = process.env.REDIS_PASSWORD || undefined;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
return new IoRedis({
|
|
29
|
+
host: parsed.hostname,
|
|
30
|
+
port: parseInt(parsed.port || '6379', 10),
|
|
31
|
+
password,
|
|
32
|
+
db: parseInt(parsed.pathname?.slice(1) || '0', 10) || 0,
|
|
33
|
+
lazyConnect: true,
|
|
34
|
+
maxRetriesPerRequest: 1,
|
|
35
|
+
enableOfflineQueue: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return new IoRedis({ host: 'redis', port: 6379, password, lazyConnect: true, maxRetriesPerRequest: 1, enableOfflineQueue: false });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let _searchRedis = null;
|
|
43
|
+
function getSearchRedis() {
|
|
44
|
+
if (!_searchRedis)
|
|
45
|
+
_searchRedis = buildSearchRedis();
|
|
46
|
+
return _searchRedis;
|
|
47
|
+
}
|
|
48
|
+
// ─── Domain filter helpers ────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Parse comma-separated domain list, normalize, and cap at 100 entries.
|
|
51
|
+
*/
|
|
52
|
+
function parseDomainList(raw) {
|
|
53
|
+
if (!raw || typeof raw !== 'string')
|
|
54
|
+
return [];
|
|
55
|
+
return raw
|
|
56
|
+
.split(',')
|
|
57
|
+
.map(d => d.trim().toLowerCase().replace(/^\./, ''))
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.slice(0, 100);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Suffix-based domain match: `reuters.com` matches `www.reuters.com`, `uk.reuters.com`, etc.
|
|
63
|
+
*/
|
|
64
|
+
function domainMatches(hostname, filterDomain) {
|
|
65
|
+
const h = hostname.toLowerCase();
|
|
66
|
+
const f = filterDomain.toLowerCase();
|
|
67
|
+
return h === f || h.endsWith('.' + f);
|
|
68
|
+
}
|
|
69
|
+
// ─── Date extraction helper ───────────────────────────────────────────────
|
|
70
|
+
const DATE_REGEX = /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+\d{1,2},?\s+\d{4}|\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4})/i;
|
|
71
|
+
/**
|
|
72
|
+
* Try to extract a Date from a snippet/title string. Returns null if no date found.
|
|
73
|
+
*/
|
|
74
|
+
function extractDateFromText(text) {
|
|
75
|
+
const match = text.match(DATE_REGEX);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const raw = match[1];
|
|
79
|
+
const parsed = new Date(raw);
|
|
80
|
+
if (!isNaN(parsed.getTime()))
|
|
81
|
+
return parsed;
|
|
82
|
+
// Try MM/DD/YYYY → rewrite to YYYY-MM-DD for parsing
|
|
83
|
+
const slashMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
84
|
+
if (slashMatch) {
|
|
85
|
+
const [, m, d, y] = slashMatch;
|
|
86
|
+
const alt = new Date(`${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`);
|
|
87
|
+
if (!isNaN(alt.getTime()))
|
|
88
|
+
return alt;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export function createSearchRouter(authStore) {
|
|
93
|
+
const router = Router();
|
|
94
|
+
// LRU cache: 15 minute TTL, max 500 entries, 50MB total size
|
|
95
|
+
const cache = new LRUCache({
|
|
96
|
+
max: 500,
|
|
97
|
+
ttl: 15 * 60 * 1000, // 15 minutes
|
|
98
|
+
maxSize: 50 * 1024 * 1024, // 50MB
|
|
99
|
+
sizeCalculation: (entry) => {
|
|
100
|
+
return JSON.stringify(entry).length;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
router.get('/v1/search', async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
// Require authentication
|
|
106
|
+
const searchAuthId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
107
|
+
if (!searchAuthId) {
|
|
108
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required. Get one free at https://app.webpeel.dev', docs: 'https://webpeel.dev/docs/api-reference#authentication' }, requestId: req.requestId });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// scrapeResults=true: fetches full page content for each result (like Firecrawl's scrape_options).
|
|
112
|
+
// Adds `content` field to each result. Significantly increases response time and credits used.
|
|
113
|
+
// Documented in OpenAPI spec under /v1/search parameters.
|
|
114
|
+
const { q, count, scrapeResults, enrich, sources, categories, tbs, country, location, local, language, structured, includeDomains: includeDomainsParam, excludeDomains: excludeDomainsParam, startDate: startDateParam, endDate: endDateParam, instant: instantParam } = req.query;
|
|
115
|
+
// --- Search provider (new: BYOK Brave support) ---
|
|
116
|
+
const providerParam = (req.query.provider || '').toLowerCase() || 'auto';
|
|
117
|
+
const validProviders = ['duckduckgo', 'brave', 'stealth', 'google', 'baidu', 'yandex', 'naver', 'yahoo_japan'];
|
|
118
|
+
let providerId = validProviders.includes(providerParam)
|
|
119
|
+
? providerParam
|
|
120
|
+
: providerParam === 'auto' ? 'auto' : 'duckduckgo';
|
|
121
|
+
// --- Auto-geo-routing: when provider=auto, detect language/region and pick best engine ---
|
|
122
|
+
const acceptLang = (req.headers['accept-language'] || '').toLowerCase();
|
|
123
|
+
const langParam = (req.query.language || '').toLowerCase();
|
|
124
|
+
let geoRoutedProvider = null;
|
|
125
|
+
if (providerId === 'auto') {
|
|
126
|
+
if (langParam.startsWith('zh') || acceptLang.startsWith('zh')) {
|
|
127
|
+
providerId = 'baidu';
|
|
128
|
+
geoRoutedProvider = 'baidu';
|
|
129
|
+
}
|
|
130
|
+
else if (langParam.startsWith('ja') || acceptLang.startsWith('ja')) {
|
|
131
|
+
providerId = 'yahoo_japan';
|
|
132
|
+
geoRoutedProvider = 'yahoo_japan';
|
|
133
|
+
}
|
|
134
|
+
else if (langParam.startsWith('ko') || acceptLang.startsWith('ko')) {
|
|
135
|
+
providerId = 'naver';
|
|
136
|
+
geoRoutedProvider = 'naver';
|
|
137
|
+
}
|
|
138
|
+
else if (langParam.startsWith('ru') || acceptLang.startsWith('ru')) {
|
|
139
|
+
providerId = 'yandex';
|
|
140
|
+
geoRoutedProvider = 'yandex';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// API key: query param, header, or empty
|
|
144
|
+
const searchApiKey = req.query.searchApiKey ||
|
|
145
|
+
req.headers['x-search-api-key'] ||
|
|
146
|
+
'';
|
|
147
|
+
// Validate query parameter
|
|
148
|
+
if (!q || typeof q !== 'string') {
|
|
149
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing or invalid "q" parameter. Pass a search query: GET /v1/search?q=your+search+terms', hint: 'Example: curl "https://api.webpeel.dev/v1/search?q=latest+AI+news&count=5"', docs: 'https://webpeel.dev/docs/api-reference#search' }, requestId: req.requestId });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Parse and validate count
|
|
153
|
+
const resultCount = count ? parseInt(count, 10) : 10;
|
|
154
|
+
if (isNaN(resultCount) || resultCount < 1 || resultCount > 20) {
|
|
155
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Invalid "count" parameter: must be between 1 and 20', hint: 'Use a count value between 1 and 20', docs: 'https://webpeel.dev/docs/errors#invalid_request' }, requestId: req.requestId });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Parse sources parameter (comma-separated: web,news,images)
|
|
159
|
+
const sourcesStr = sources || 'web';
|
|
160
|
+
const sourcesArray = sourcesStr.split(',').map(s => s.trim());
|
|
161
|
+
const shouldScrape = scrapeResults === 'true';
|
|
162
|
+
// Parse new search parameters
|
|
163
|
+
const categoriesStr = categories || '';
|
|
164
|
+
const tbsStr = tbs || '';
|
|
165
|
+
const countryStr = country || '';
|
|
166
|
+
const locationStr = location || '';
|
|
167
|
+
const languageStr = language || '';
|
|
168
|
+
const isLocalSearch = local === 'true' || local === '1';
|
|
169
|
+
// ── Local search shortcut ─────────────────────────────────────────────
|
|
170
|
+
// When local=true, route through Google Places / Yelp instead of web search.
|
|
171
|
+
if (isLocalSearch) {
|
|
172
|
+
const localStartTime = Date.now();
|
|
173
|
+
try {
|
|
174
|
+
const localResponse = await localSearch({
|
|
175
|
+
query: q,
|
|
176
|
+
location: locationStr || undefined,
|
|
177
|
+
country: countryStr || undefined,
|
|
178
|
+
language: languageStr || undefined,
|
|
179
|
+
limit: resultCount,
|
|
180
|
+
});
|
|
181
|
+
const localElapsed = Date.now() - localStartTime;
|
|
182
|
+
// Track usage
|
|
183
|
+
const pgStoreLocal = authStore;
|
|
184
|
+
if (req.auth?.keyInfo?.key && typeof pgStoreLocal.trackUsage === 'function') {
|
|
185
|
+
await pgStoreLocal.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
186
|
+
}
|
|
187
|
+
res.setHeader('X-Cache', 'MISS');
|
|
188
|
+
res.setHeader('X-Cache-Status', 'MISS');
|
|
189
|
+
res.setHeader('X-Credits-Used', '1');
|
|
190
|
+
res.setHeader('X-Processing-Time', localElapsed.toString());
|
|
191
|
+
res.setHeader('X-Fetch-Type', 'local-search');
|
|
192
|
+
res.setHeader('X-Local-Source', localResponse.source);
|
|
193
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
194
|
+
res.json({
|
|
195
|
+
success: true,
|
|
196
|
+
data: {
|
|
197
|
+
local: localResponse.results,
|
|
198
|
+
source: localResponse.source,
|
|
199
|
+
query: localResponse.query,
|
|
200
|
+
location: localResponse.location,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
catch (localErr) {
|
|
206
|
+
console.error('[search] Local search error:', localErr);
|
|
207
|
+
res.status(500).json({
|
|
208
|
+
success: false,
|
|
209
|
+
error: {
|
|
210
|
+
type: 'local_search_failed',
|
|
211
|
+
message: 'Local search failed. Ensure GOOGLE_PLACES_API_KEY or YELP_API_KEY is configured.',
|
|
212
|
+
hint: 'Set GOOGLE_PLACES_API_KEY env var for best results. Without API keys, uses scraping fallback.',
|
|
213
|
+
},
|
|
214
|
+
requestId: req.requestId,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ── Parse domain and date filter params ──────────────────────────────
|
|
220
|
+
const includeDomains = parseDomainList(includeDomainsParam);
|
|
221
|
+
const excludeDomains = parseDomainList(excludeDomainsParam);
|
|
222
|
+
const startDate = startDateParam ? new Date(startDateParam) : null;
|
|
223
|
+
const endDate = endDateParam ? new Date(endDateParam) : null;
|
|
224
|
+
const isInstant = instantParam === 'true' || instantParam === '1';
|
|
225
|
+
// Validate date params if provided
|
|
226
|
+
if (startDateParam && (!startDate || isNaN(startDate.getTime()))) {
|
|
227
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Invalid "startDate" parameter. Use ISO 8601 format (e.g., "2026-01-01").', docs: 'https://webpeel.dev/docs/api-reference#search' }, requestId: req.requestId });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (endDateParam && (!endDate || isNaN(endDate.getTime()))) {
|
|
231
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Invalid "endDate" parameter. Use ISO 8601 format (e.g., "2026-12-31").', docs: 'https://webpeel.dev/docs/api-reference#search' }, requestId: req.requestId });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Build cache key (include all parameters — domain/date filters affect results)
|
|
235
|
+
const enrichCount = enrich ? Math.min(Math.max(parseInt(enrich, 10) || 0, 0), 5) : 0;
|
|
236
|
+
const isStructured = structured === 'true' || structured === '1';
|
|
237
|
+
const filterSuffix = [
|
|
238
|
+
includeDomains.length ? `inc:${includeDomains.join('|')}` : '',
|
|
239
|
+
excludeDomains.length ? `exc:${excludeDomains.join('|')}` : '',
|
|
240
|
+
startDateParam ? `sd:${startDateParam}` : '',
|
|
241
|
+
endDateParam ? `ed:${endDateParam}` : '',
|
|
242
|
+
].filter(Boolean).join(':');
|
|
243
|
+
const cacheKey = `search:${providerId}:${q}:${resultCount}:${sourcesStr}:${shouldScrape}:${enrichCount}:${categoriesStr}:${tbsStr}:${countryStr}:${locationStr}:${isStructured}${filterSuffix ? ':' + filterSuffix : ''}`;
|
|
244
|
+
const sharedCacheKey = searchCache.getKey(cacheKey, {});
|
|
245
|
+
// ── Redis instant cache (30-min TTL, checked BEFORE LRU) ────────────
|
|
246
|
+
const redisInstantKey = `search:instant:${cacheKey}`;
|
|
247
|
+
try {
|
|
248
|
+
const redis = getSearchRedis();
|
|
249
|
+
const redisCached = await redis.get(redisInstantKey);
|
|
250
|
+
if (redisCached) {
|
|
251
|
+
const parsed = JSON.parse(redisCached);
|
|
252
|
+
const age = Math.floor((Date.now() - parsed.timestamp) / 1000);
|
|
253
|
+
res.setHeader('X-Cache', 'INSTANT');
|
|
254
|
+
res.setHeader('X-Cache-Status', 'INSTANT');
|
|
255
|
+
res.setHeader('X-Cache-Age', age.toString());
|
|
256
|
+
res.json({
|
|
257
|
+
success: true,
|
|
258
|
+
data: parsed.data,
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// If instant=true and nothing in Redis, return 404 (instant-only mode)
|
|
263
|
+
if (isInstant) {
|
|
264
|
+
res.status(404).json({
|
|
265
|
+
success: false,
|
|
266
|
+
error: {
|
|
267
|
+
type: 'not_cached',
|
|
268
|
+
message: 'No cached result available for this query. Remove instant=true to perform a live search.',
|
|
269
|
+
docs: 'https://webpeel.dev/docs/api-reference#search',
|
|
270
|
+
},
|
|
271
|
+
requestId: req.requestId,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
// Redis unavailable — graceful degradation, continue to LRU
|
|
278
|
+
if (process.env.DEBUG)
|
|
279
|
+
console.debug('[search] Redis instant cache error (non-fatal):', err.message);
|
|
280
|
+
// If instant=true and Redis is down, we can't serve cached results
|
|
281
|
+
if (isInstant) {
|
|
282
|
+
res.status(503).json({
|
|
283
|
+
success: false,
|
|
284
|
+
error: {
|
|
285
|
+
type: 'cache_unavailable',
|
|
286
|
+
message: 'Instant cache is temporarily unavailable. Try again without instant=true.',
|
|
287
|
+
docs: 'https://webpeel.dev/docs/api-reference#search',
|
|
288
|
+
},
|
|
289
|
+
requestId: req.requestId,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Check cache (local LRU first, then shared singleton)
|
|
295
|
+
const cached = cache.get(cacheKey);
|
|
296
|
+
if (cached) {
|
|
297
|
+
res.setHeader('X-Cache', 'HIT');
|
|
298
|
+
res.setHeader('X-Cache-Status', 'HIT');
|
|
299
|
+
res.setHeader('X-Cache-Age', Math.floor((Date.now() - cached.timestamp) / 1000).toString());
|
|
300
|
+
res.json({
|
|
301
|
+
success: true,
|
|
302
|
+
data: cached.data,
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Also check shared searchCache singleton (used for /health stats)
|
|
307
|
+
const sharedCached = searchCache.get(sharedCacheKey);
|
|
308
|
+
if (sharedCached) {
|
|
309
|
+
const age = Math.floor((Date.now() - sharedCached.timestamp) / 1000);
|
|
310
|
+
res.setHeader('X-Cache', 'HIT');
|
|
311
|
+
res.setHeader('X-Cache-Status', 'HIT');
|
|
312
|
+
res.setHeader('X-Cache-Age', age.toString());
|
|
313
|
+
res.json({
|
|
314
|
+
success: true,
|
|
315
|
+
data: sharedCached.content ? JSON.parse(sharedCached.content) : {},
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const startTime = Date.now();
|
|
320
|
+
const data = {};
|
|
321
|
+
// Fetch web results via the search-provider abstraction
|
|
322
|
+
if (sourcesArray.includes('web')) {
|
|
323
|
+
// When provider=auto (default), use getBestSearchProvider which picks
|
|
324
|
+
// the best available provider based on configured API keys.
|
|
325
|
+
// When a specific provider is requested, use that directly.
|
|
326
|
+
let searchProvider;
|
|
327
|
+
let effectiveApiKey;
|
|
328
|
+
if (providerId === 'auto') {
|
|
329
|
+
const best = getBestSearchProvider();
|
|
330
|
+
searchProvider = best.provider;
|
|
331
|
+
effectiveApiKey = searchApiKey || best.apiKey;
|
|
332
|
+
}
|
|
333
|
+
else if (providerId === 'baidu') {
|
|
334
|
+
searchProvider = new BaiduSearchProvider();
|
|
335
|
+
effectiveApiKey = undefined;
|
|
336
|
+
}
|
|
337
|
+
else if (providerId === 'yandex') {
|
|
338
|
+
searchProvider = new YandexSearchProvider();
|
|
339
|
+
effectiveApiKey = undefined;
|
|
340
|
+
}
|
|
341
|
+
else if (providerId === 'naver') {
|
|
342
|
+
searchProvider = new NaverSearchProvider();
|
|
343
|
+
effectiveApiKey = undefined;
|
|
344
|
+
}
|
|
345
|
+
else if (providerId === 'yahoo_japan') {
|
|
346
|
+
searchProvider = new YahooJapanSearchProvider();
|
|
347
|
+
effectiveApiKey = undefined;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
searchProvider = getSearchProvider(providerId);
|
|
351
|
+
effectiveApiKey = searchApiKey || undefined;
|
|
352
|
+
}
|
|
353
|
+
let providerResults = await searchProvider.searchWeb(q, {
|
|
354
|
+
count: resultCount,
|
|
355
|
+
apiKey: effectiveApiKey,
|
|
356
|
+
tbs: tbsStr || undefined,
|
|
357
|
+
country: countryStr || undefined,
|
|
358
|
+
location: locationStr || undefined,
|
|
359
|
+
structured: isStructured,
|
|
360
|
+
});
|
|
361
|
+
// ── Domain filtering (suffix-based) ──────────────────────────────
|
|
362
|
+
if (includeDomains.length > 0) {
|
|
363
|
+
providerResults = providerResults.filter(r => {
|
|
364
|
+
try {
|
|
365
|
+
const hostname = new URL(r.url).hostname.toLowerCase();
|
|
366
|
+
return includeDomains.some(d => domainMatches(hostname, d));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (excludeDomains.length > 0) {
|
|
374
|
+
providerResults = providerResults.filter(r => {
|
|
375
|
+
try {
|
|
376
|
+
const hostname = new URL(r.url).hostname.toLowerCase();
|
|
377
|
+
return !excludeDomains.some(d => domainMatches(hostname, d));
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// ── Date filtering (fuzzy, from snippet/title text) ──────────────
|
|
385
|
+
if (startDate || endDate) {
|
|
386
|
+
providerResults = providerResults.filter(r => {
|
|
387
|
+
const text = `${r.title} ${r.snippet}`;
|
|
388
|
+
const detected = extractDateFromText(text);
|
|
389
|
+
if (!detected)
|
|
390
|
+
return true; // Keep undatable results
|
|
391
|
+
if (startDate && detected < startDate)
|
|
392
|
+
return false;
|
|
393
|
+
if (endDate && detected > endDate)
|
|
394
|
+
return false;
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// Map to SearchResult (with optional content field)
|
|
399
|
+
let results = providerResults.map(r => ({
|
|
400
|
+
title: r.title,
|
|
401
|
+
url: r.url,
|
|
402
|
+
snippet: r.snippet,
|
|
403
|
+
...(r.serp ? { serp: r.serp } : {}),
|
|
404
|
+
}));
|
|
405
|
+
// Apply category filtering if specified
|
|
406
|
+
if (categoriesStr) {
|
|
407
|
+
const categoryList = categoriesStr.split(',').map(c => c.trim().toLowerCase());
|
|
408
|
+
results = results.filter(result => {
|
|
409
|
+
const urlLower = result.url.toLowerCase();
|
|
410
|
+
return categoryList.some(category => {
|
|
411
|
+
switch (category) {
|
|
412
|
+
case 'github':
|
|
413
|
+
return urlLower.includes('github.com');
|
|
414
|
+
case 'pdf':
|
|
415
|
+
return urlLower.endsWith('.pdf');
|
|
416
|
+
case 'docs':
|
|
417
|
+
case 'documentation':
|
|
418
|
+
return urlLower.includes('/docs') || urlLower.includes('/documentation');
|
|
419
|
+
case 'blog':
|
|
420
|
+
return urlLower.includes('blog') || urlLower.includes('/post/');
|
|
421
|
+
case 'news':
|
|
422
|
+
return urlLower.includes('news') || urlLower.includes('/article/');
|
|
423
|
+
case 'video':
|
|
424
|
+
return urlLower.includes('youtube.com') || urlLower.includes('vimeo.com');
|
|
425
|
+
case 'social':
|
|
426
|
+
return urlLower.includes('twitter.com') || urlLower.includes('x.com') ||
|
|
427
|
+
urlLower.includes('facebook.com') || urlLower.includes('linkedin.com');
|
|
428
|
+
default:
|
|
429
|
+
return urlLower.includes(category);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// Scrape each result URL if requested (sequential — legacy)
|
|
435
|
+
if (shouldScrape) {
|
|
436
|
+
for (const result of results) {
|
|
437
|
+
try {
|
|
438
|
+
const peelResult = await peel(result.url, {
|
|
439
|
+
format: 'markdown',
|
|
440
|
+
maxTokens: 2000,
|
|
441
|
+
});
|
|
442
|
+
result.content = peelResult.content;
|
|
443
|
+
// Attach full trust score from pipeline
|
|
444
|
+
if (peelResult.trust) {
|
|
445
|
+
result.trust = {
|
|
446
|
+
score: peelResult.trust.score,
|
|
447
|
+
tier: peelResult.trust.source.tier,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
result.content = `[Failed to scrape: ${error.message}]`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Lightweight enrichment — HTTP-only, no browser, no full pipeline
|
|
457
|
+
// Uses simpleFetch + cheerio to extract text without launching Playwright
|
|
458
|
+
// This is intentionally minimal to stay within 512MB container memory limit
|
|
459
|
+
if (enrichCount > 0 && !shouldScrape) {
|
|
460
|
+
const ENRICH_TIMEOUT = 4000;
|
|
461
|
+
const toEnrich = results.slice(0, enrichCount);
|
|
462
|
+
const enrichResults = await Promise.allSettled(toEnrich.map(async (result) => {
|
|
463
|
+
const t0 = Date.now();
|
|
464
|
+
const fetchPromise = (async () => {
|
|
465
|
+
const fetched = await simpleFetch(result.url, undefined, ENRICH_TIMEOUT);
|
|
466
|
+
if (!fetched.html)
|
|
467
|
+
return { url: result.url, content: null, wordCount: 0, method: 'empty', fetchTimeMs: 0 };
|
|
468
|
+
// Extract visible text with cheerio — lightweight, no full pipeline
|
|
469
|
+
const $ = load(fetched.html);
|
|
470
|
+
$('script, style, nav, header, footer, [aria-hidden="true"], .ad, .advertisement').remove();
|
|
471
|
+
// Try main content selectors first, then body
|
|
472
|
+
const mainEl = $('main, article, [role="main"], .content, .article-body, #content').first();
|
|
473
|
+
const textEl = mainEl.length ? mainEl : $('body');
|
|
474
|
+
const text = textEl.text().replace(/\s+/g, ' ').trim().substring(0, 2000);
|
|
475
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
476
|
+
return {
|
|
477
|
+
url: result.url,
|
|
478
|
+
content: text.substring(0, 1500) || null,
|
|
479
|
+
wordCount,
|
|
480
|
+
method: 'simple',
|
|
481
|
+
fetchTimeMs: Date.now() - t0,
|
|
482
|
+
};
|
|
483
|
+
})();
|
|
484
|
+
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve({ url: result.url, content: null, wordCount: 0, method: 'timeout', fetchTimeMs: ENRICH_TIMEOUT }), ENRICH_TIMEOUT));
|
|
485
|
+
return Promise.race([fetchPromise, timeoutPromise]);
|
|
486
|
+
}));
|
|
487
|
+
// Merge enrichment data back into results
|
|
488
|
+
for (const settled of enrichResults) {
|
|
489
|
+
if (settled.status === 'fulfilled' && settled.value.content) {
|
|
490
|
+
const match = results.find(r => r.url === settled.value.url);
|
|
491
|
+
if (match) {
|
|
492
|
+
match.content = settled.value.content;
|
|
493
|
+
match.wordCount = settled.value.wordCount;
|
|
494
|
+
match.method = settled.value.method;
|
|
495
|
+
match.fetchTimeMs = settled.value.fetchTimeMs;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Add credibility scores and sort by trustworthiness
|
|
501
|
+
const tierOrder = { official: 0, established: 1, community: 2, new: 3, suspicious: 4 };
|
|
502
|
+
results = results
|
|
503
|
+
.map(r => {
|
|
504
|
+
const cred = getSourceCredibility(r.url);
|
|
505
|
+
// Add lightweight trust score (heuristic only, no network) if not already set by scraper
|
|
506
|
+
const trust = r.trust ?? {
|
|
507
|
+
score: Math.round(Math.max(0, Math.min(100, cred.score))) / 100,
|
|
508
|
+
tier: cred.tier,
|
|
509
|
+
};
|
|
510
|
+
return { ...r, credibility: cred, trust };
|
|
511
|
+
})
|
|
512
|
+
.sort((a, b) => {
|
|
513
|
+
const aTier = tierOrder[a.credibility?.tier || 'new'] ?? 3;
|
|
514
|
+
const bTier = tierOrder[b.credibility?.tier || 'new'] ?? 3;
|
|
515
|
+
return aTier - bTier; // Official first, then established, community, new, suspicious
|
|
516
|
+
})
|
|
517
|
+
.map((r, i) => ({ ...r, rank: i + 1 }));
|
|
518
|
+
data.web = results;
|
|
519
|
+
}
|
|
520
|
+
// Fetch news results (DDG only — Brave news is not supported via HTML scraping)
|
|
521
|
+
if (sourcesArray.includes('news')) {
|
|
522
|
+
const newsUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(q)}&t=news`;
|
|
523
|
+
const response = await undiciFetch(newsUrl, {
|
|
524
|
+
headers: {
|
|
525
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
if (response.ok) {
|
|
529
|
+
const html = await response.text();
|
|
530
|
+
const $ = load(html);
|
|
531
|
+
const results = [];
|
|
532
|
+
$('.result').each((_i, elem) => {
|
|
533
|
+
if (results.length >= resultCount)
|
|
534
|
+
return;
|
|
535
|
+
const $result = $(elem);
|
|
536
|
+
let title = $result.find('.result__title').text().trim();
|
|
537
|
+
const rawUrl = $result.find('.result__a').attr('href') || '';
|
|
538
|
+
let snippet = $result.find('.result__snippet').text().trim();
|
|
539
|
+
const sourceText = $result.find('.result__extras__url').text().trim();
|
|
540
|
+
if (!title || !rawUrl)
|
|
541
|
+
return;
|
|
542
|
+
let url = rawUrl;
|
|
543
|
+
try {
|
|
544
|
+
const ddgUrl = new URL(rawUrl, 'https://duckduckgo.com');
|
|
545
|
+
const uddg = ddgUrl.searchParams.get('uddg');
|
|
546
|
+
if (uddg) {
|
|
547
|
+
url = decodeURIComponent(uddg);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
if (process.env.DEBUG)
|
|
552
|
+
console.debug('[webpeel]', 'ddg url parse failed:', e instanceof Error ? e.message : e);
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const parsed = new URL(url);
|
|
556
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
url = parsed.href;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
title = title.slice(0, 200);
|
|
565
|
+
snippet = snippet.slice(0, 500);
|
|
566
|
+
results.push({
|
|
567
|
+
title,
|
|
568
|
+
url,
|
|
569
|
+
snippet,
|
|
570
|
+
source: sourceText.slice(0, 100),
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
data.news = results;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Fetch image results (DDG only)
|
|
577
|
+
if (sourcesArray.includes('images')) {
|
|
578
|
+
const imagesUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(q)}&t=images`;
|
|
579
|
+
const response = await undiciFetch(imagesUrl, {
|
|
580
|
+
headers: {
|
|
581
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
if (response.ok) {
|
|
585
|
+
const html = await response.text();
|
|
586
|
+
const $ = load(html);
|
|
587
|
+
const results = [];
|
|
588
|
+
$('.result').each((_i, elem) => {
|
|
589
|
+
if (results.length >= resultCount)
|
|
590
|
+
return;
|
|
591
|
+
const $result = $(elem);
|
|
592
|
+
const title = $result.find('.result__title').text().trim();
|
|
593
|
+
const thumbnail = $result.find('.result__image img').attr('src') || '';
|
|
594
|
+
const rawUrl = $result.find('.result__a').attr('href') || '';
|
|
595
|
+
const sourceText = $result.find('.result__extras__url').text().trim();
|
|
596
|
+
if (!title || !rawUrl || !thumbnail)
|
|
597
|
+
return;
|
|
598
|
+
let url = rawUrl;
|
|
599
|
+
try {
|
|
600
|
+
const ddgUrl = new URL(rawUrl, 'https://duckduckgo.com');
|
|
601
|
+
const uddg = ddgUrl.searchParams.get('uddg');
|
|
602
|
+
if (uddg) {
|
|
603
|
+
url = decodeURIComponent(uddg);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
if (process.env.DEBUG)
|
|
608
|
+
console.debug('[webpeel]', 'ddg url parse failed:', e instanceof Error ? e.message : e);
|
|
609
|
+
}
|
|
610
|
+
results.push({
|
|
611
|
+
title: title.slice(0, 200),
|
|
612
|
+
url,
|
|
613
|
+
thumbnail,
|
|
614
|
+
source: sourceText.slice(0, 100),
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
data.images = results;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const elapsed = Date.now() - startTime;
|
|
621
|
+
// Track usage
|
|
622
|
+
const isSoftLimited = req.auth?.softLimited === true;
|
|
623
|
+
const hasExtraUsage = req.auth?.extraUsageAvailable === true;
|
|
624
|
+
const pgStore = authStore;
|
|
625
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackBurstUsage === 'function') {
|
|
626
|
+
// Track burst usage (always)
|
|
627
|
+
await pgStore.trackBurstUsage(req.auth.keyInfo.key);
|
|
628
|
+
// If soft-limited with extra usage available, charge to extra usage
|
|
629
|
+
if (isSoftLimited && hasExtraUsage) {
|
|
630
|
+
const extraResult = await pgStore.trackExtraUsage(req.auth.keyInfo.key, 'search', `search:${q}`, elapsed, 200);
|
|
631
|
+
if (extraResult.success) {
|
|
632
|
+
res.setHeader('X-Extra-Usage-Charged', `$${extraResult.cost.toFixed(4)}`);
|
|
633
|
+
res.setHeader('X-Extra-Usage-New-Balance', extraResult.newBalance.toFixed(2));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (!isSoftLimited) {
|
|
637
|
+
// Normal weekly usage tracking
|
|
638
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search');
|
|
639
|
+
}
|
|
640
|
+
// Automatic dual-threshold alerts (80% and 90%)
|
|
641
|
+
if (req.auth?.keyInfo?.accountId && typeof pgStore.pool !== 'undefined') {
|
|
642
|
+
checkAndSendDualAlert(pgStore.pool, req.auth.keyInfo.accountId).catch(() => { });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Cache results (local LRU + shared singleton for /health stats + Redis instant)
|
|
646
|
+
const cacheTimestamp = Date.now();
|
|
647
|
+
cache.set(cacheKey, {
|
|
648
|
+
data,
|
|
649
|
+
timestamp: cacheTimestamp,
|
|
650
|
+
});
|
|
651
|
+
searchCache.set(sharedCacheKey, {
|
|
652
|
+
content: JSON.stringify(data),
|
|
653
|
+
title: q,
|
|
654
|
+
metadata: {},
|
|
655
|
+
method: 'search',
|
|
656
|
+
tokens: 0,
|
|
657
|
+
timestamp: cacheTimestamp,
|
|
658
|
+
});
|
|
659
|
+
// Write to Redis for instant cache (30-min TTL)
|
|
660
|
+
try {
|
|
661
|
+
const redis = getSearchRedis();
|
|
662
|
+
await redis.setex(redisInstantKey, 1800, JSON.stringify({ data, timestamp: cacheTimestamp }));
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
if (process.env.DEBUG)
|
|
666
|
+
console.debug('[search] Redis instant cache write error (non-fatal):', err.message);
|
|
667
|
+
}
|
|
668
|
+
// Add headers
|
|
669
|
+
res.setHeader('X-Cache', 'MISS');
|
|
670
|
+
res.setHeader('X-Cache-Status', 'MISS');
|
|
671
|
+
res.setHeader('X-Credits-Used', '1');
|
|
672
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
673
|
+
res.setHeader('X-Fetch-Type', 'search');
|
|
674
|
+
res.setHeader('Cache-Control', 'no-store'); // Never cache search results — they must be fresh
|
|
675
|
+
if (geoRoutedProvider) {
|
|
676
|
+
res.setHeader('X-Geo-Provider', geoRoutedProvider);
|
|
677
|
+
}
|
|
678
|
+
res.json({
|
|
679
|
+
success: true,
|
|
680
|
+
data,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
const err = error;
|
|
685
|
+
// SECURITY: Generic error message to prevent information disclosure
|
|
686
|
+
console.error('Search error:', err); // Log full error server-side
|
|
687
|
+
res.status(500).json({
|
|
688
|
+
success: false,
|
|
689
|
+
error: {
|
|
690
|
+
type: 'search_failed',
|
|
691
|
+
message: 'Search request failed. If using Brave provider, verify your API key. Otherwise try again.',
|
|
692
|
+
hint: 'Free search uses DuckDuckGo (no key required). For higher quality, add provider=brave&searchApiKey=YOUR_KEY',
|
|
693
|
+
docs: 'https://webpeel.dev/docs/api-reference#search',
|
|
694
|
+
},
|
|
695
|
+
requestId: req.requestId,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
// ── GET /v1/search/shopping ──────────────────────────────────────────────
|
|
700
|
+
router.get('/v1/search/shopping', async (req, res) => {
|
|
701
|
+
const authId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
702
|
+
if (!authId) {
|
|
703
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required.' } });
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const { q, count, country, language } = req.query;
|
|
707
|
+
if (!q || typeof q !== 'string') {
|
|
708
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing required "q" parameter.' } });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const resultCount = count ? Math.min(Math.max(parseInt(count, 10) || 10, 1), 40) : 10;
|
|
712
|
+
const startTime = Date.now();
|
|
713
|
+
try {
|
|
714
|
+
const results = await searchShopping({
|
|
715
|
+
query: q,
|
|
716
|
+
count: resultCount,
|
|
717
|
+
country: country,
|
|
718
|
+
language: language,
|
|
719
|
+
});
|
|
720
|
+
const elapsed = Date.now() - startTime;
|
|
721
|
+
const pgStore = authStore;
|
|
722
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackUsage === 'function') {
|
|
723
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
724
|
+
}
|
|
725
|
+
res.setHeader('X-Credits-Used', '1');
|
|
726
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
727
|
+
res.json({ success: true, data: { results, query: q, count: results.length, elapsed } });
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
console.error('[search/shopping] error:', err);
|
|
731
|
+
res.status(500).json({ success: false, error: { type: 'search_failed', message: 'Shopping search failed.' } });
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
// ── GET /v1/search/news ──────────────────────────────────────────────────
|
|
735
|
+
router.get('/v1/search/news', async (req, res) => {
|
|
736
|
+
const authId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
737
|
+
if (!authId) {
|
|
738
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required.' } });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const { q, count, language, freshness } = req.query;
|
|
742
|
+
if (!q || typeof q !== 'string') {
|
|
743
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing required "q" parameter.' } });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const resultCount = count ? Math.min(Math.max(parseInt(count, 10) || 10, 1), 40) : 10;
|
|
747
|
+
const startTime = Date.now();
|
|
748
|
+
try {
|
|
749
|
+
const results = await searchNewsVertical({
|
|
750
|
+
query: q,
|
|
751
|
+
count: resultCount,
|
|
752
|
+
language: language,
|
|
753
|
+
freshness: freshness,
|
|
754
|
+
});
|
|
755
|
+
const elapsed = Date.now() - startTime;
|
|
756
|
+
const pgStore = authStore;
|
|
757
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackUsage === 'function') {
|
|
758
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
759
|
+
}
|
|
760
|
+
res.setHeader('X-Credits-Used', '1');
|
|
761
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
762
|
+
res.json({ success: true, data: { results, query: q, count: results.length, elapsed } });
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
console.error('[search/news] error:', err);
|
|
766
|
+
res.status(500).json({ success: false, error: { type: 'search_failed', message: 'News search failed.' } });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
// ── GET /v1/search/images ────────────────────────────────────────────────
|
|
770
|
+
router.get('/v1/search/images', async (req, res) => {
|
|
771
|
+
const authId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
772
|
+
if (!authId) {
|
|
773
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required.' } });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const { q, count, country, language } = req.query;
|
|
777
|
+
if (!q || typeof q !== 'string') {
|
|
778
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing required "q" parameter.' } });
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const resultCount = count ? Math.min(Math.max(parseInt(count, 10) || 20, 1), 50) : 20;
|
|
782
|
+
const startTime = Date.now();
|
|
783
|
+
try {
|
|
784
|
+
const results = await searchImagesVertical({
|
|
785
|
+
query: q,
|
|
786
|
+
count: resultCount,
|
|
787
|
+
country: country,
|
|
788
|
+
language: language,
|
|
789
|
+
});
|
|
790
|
+
const elapsed = Date.now() - startTime;
|
|
791
|
+
const pgStore = authStore;
|
|
792
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackUsage === 'function') {
|
|
793
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
794
|
+
}
|
|
795
|
+
res.setHeader('X-Credits-Used', '1');
|
|
796
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
797
|
+
res.json({ success: true, data: { results, query: q, count: results.length, elapsed } });
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
console.error('[search/images] error:', err);
|
|
801
|
+
res.status(500).json({ success: false, error: { type: 'search_failed', message: 'Image search failed.' } });
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
// ── GET /v1/search/verify ────────────────────────────────────────────────
|
|
805
|
+
// Cross-source verification: searches multiple engines and computes consensus
|
|
806
|
+
// GET /v1/search/verify?q=...&engines=google,duckduckgo,baidu&count=10
|
|
807
|
+
router.get('/v1/search/verify', async (req, res) => {
|
|
808
|
+
const authId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
809
|
+
if (!authId) {
|
|
810
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required.' } });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const { q, engines, count } = req.query;
|
|
814
|
+
if (!q || typeof q !== 'string') {
|
|
815
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing required "q" parameter.' } });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const engineList = engines
|
|
819
|
+
? engines.split(',').map(e => e.trim()).filter(Boolean)
|
|
820
|
+
: undefined;
|
|
821
|
+
const resultCount = count ? Math.min(Math.max(parseInt(count, 10) || 10, 1), 20) : 10;
|
|
822
|
+
const startTime = Date.now();
|
|
823
|
+
try {
|
|
824
|
+
const result = await crossVerifySearch(q, { engines: engineList, count: resultCount });
|
|
825
|
+
const elapsed = Date.now() - startTime;
|
|
826
|
+
const pgStore = authStore;
|
|
827
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackUsage === 'function') {
|
|
828
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
829
|
+
}
|
|
830
|
+
res.setHeader('X-Credits-Used', '1');
|
|
831
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
832
|
+
res.json({ success: true, data: result });
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
console.error('[search/verify] error:', err);
|
|
836
|
+
res.status(500).json({ success: false, error: { type: 'search_failed', message: 'Cross-verify search failed.' } });
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
// ── GET /v1/search/videos ────────────────────────────────────────────────
|
|
840
|
+
router.get('/v1/search/videos', async (req, res) => {
|
|
841
|
+
const authId = req.auth?.keyInfo?.accountId || req.user?.userId;
|
|
842
|
+
if (!authId) {
|
|
843
|
+
res.status(401).json({ success: false, error: { type: 'authentication_required', message: 'API key required.' } });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const { q, count, language } = req.query;
|
|
847
|
+
if (!q || typeof q !== 'string') {
|
|
848
|
+
res.status(400).json({ success: false, error: { type: 'invalid_request', message: 'Missing required "q" parameter.' } });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const resultCount = count ? Math.min(Math.max(parseInt(count, 10) || 10, 1), 20) : 10;
|
|
852
|
+
const startTime = Date.now();
|
|
853
|
+
try {
|
|
854
|
+
const results = await searchVideos({
|
|
855
|
+
query: q,
|
|
856
|
+
count: resultCount,
|
|
857
|
+
language: language,
|
|
858
|
+
});
|
|
859
|
+
const elapsed = Date.now() - startTime;
|
|
860
|
+
const pgStore = authStore;
|
|
861
|
+
if (req.auth?.keyInfo?.key && typeof pgStore.trackUsage === 'function') {
|
|
862
|
+
await pgStore.trackUsage(req.auth.keyInfo.key, 'search').catch(() => { });
|
|
863
|
+
}
|
|
864
|
+
res.setHeader('X-Credits-Used', '1');
|
|
865
|
+
res.setHeader('X-Processing-Time', elapsed.toString());
|
|
866
|
+
res.json({ success: true, data: { results, query: q, count: results.length, elapsed } });
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
console.error('[search/videos] error:', err);
|
|
870
|
+
res.status(500).json({ success: false, error: { type: 'search_failed', message: 'Video search failed.' } });
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
return router;
|
|
874
|
+
}
|