@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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed rate limiting middleware.
|
|
3
|
+
*
|
|
4
|
+
* Uses `rate-limiter-flexible` for atomic Redis operations — works correctly
|
|
5
|
+
* across all 6 K8s API pods (shared state via Redis).
|
|
6
|
+
*
|
|
7
|
+
* Falls back to in-memory when REDIS_URL is not set (CLI/local dev).
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Per-key (API key or IP) rate limiting
|
|
11
|
+
* - Tier-based limits: free=50/hr, pro=100/hr, max=500/hr
|
|
12
|
+
* - Route-based cost weighting: crawl=5x, render=3x, batch/screenshot=2x
|
|
13
|
+
* - Per-IP rate limiting ON TOP of API key limits (abuse prevention)
|
|
14
|
+
* - Exempt paths for health/docs endpoints
|
|
15
|
+
*/
|
|
16
|
+
import '../types.js';
|
|
17
|
+
// ─── Tier burst limits ───────────────────────────────────────────────────────
|
|
18
|
+
const TIER_BURST_LIMITS = {
|
|
19
|
+
free: 50,
|
|
20
|
+
pro: 100,
|
|
21
|
+
max: 500,
|
|
22
|
+
admin: 999999,
|
|
23
|
+
};
|
|
24
|
+
/** Global per-IP limit regardless of API key (prevents shared-key abuse) */
|
|
25
|
+
const GLOBAL_IP_LIMIT = 1000; // per hour
|
|
26
|
+
// ─── Exempt paths ────────────────────────────────────────────────────────────
|
|
27
|
+
const EXEMPT_PATHS = [
|
|
28
|
+
'/health',
|
|
29
|
+
'/ready',
|
|
30
|
+
'/openapi.json',
|
|
31
|
+
'/openapi.yaml',
|
|
32
|
+
'/docs',
|
|
33
|
+
'/v1/usage',
|
|
34
|
+
'/v1/me',
|
|
35
|
+
'/v1/keys',
|
|
36
|
+
'/v1/activity',
|
|
37
|
+
'/v1/stats',
|
|
38
|
+
];
|
|
39
|
+
// ─── RateLimiter class (Redis or in-memory) ──────────────────────────────────
|
|
40
|
+
export class RateLimiter {
|
|
41
|
+
keyLimiter; // RateLimiterRedis or RateLimiterMemory
|
|
42
|
+
ipLimiter; // separate limiter for per-IP limits
|
|
43
|
+
windowMs;
|
|
44
|
+
initialized = false;
|
|
45
|
+
initPromise;
|
|
46
|
+
constructor(windowMs = 3_600_000) {
|
|
47
|
+
this.windowMs = windowMs;
|
|
48
|
+
this.initPromise = this.init();
|
|
49
|
+
}
|
|
50
|
+
async init() {
|
|
51
|
+
const duration = Math.floor(this.windowMs / 1000); // seconds
|
|
52
|
+
try {
|
|
53
|
+
// Try Redis first
|
|
54
|
+
const redisUrl = process.env.REDIS_URL;
|
|
55
|
+
if (redisUrl) {
|
|
56
|
+
// Dynamic import to avoid hard dependency
|
|
57
|
+
const [{ RateLimiterRedis }, IoRedisModule] = await Promise.all([
|
|
58
|
+
import('rate-limiter-flexible'),
|
|
59
|
+
import('ioredis'),
|
|
60
|
+
]);
|
|
61
|
+
const IoRedis = IoRedisModule.default ?? IoRedisModule;
|
|
62
|
+
const parsed = new URL(redisUrl);
|
|
63
|
+
const redisClient = new IoRedis({
|
|
64
|
+
host: parsed.hostname,
|
|
65
|
+
port: parseInt(parsed.port || '6379', 10),
|
|
66
|
+
password: process.env.REDIS_PASSWORD || undefined,
|
|
67
|
+
db: parseInt(parsed.pathname?.slice(1) || '0', 10) || 0,
|
|
68
|
+
enableOfflineQueue: false,
|
|
69
|
+
maxRetriesPerRequest: 1,
|
|
70
|
+
lazyConnect: true,
|
|
71
|
+
});
|
|
72
|
+
await redisClient.connect();
|
|
73
|
+
// Key-based limiter (per API key or unauthenticated IP)
|
|
74
|
+
this.keyLimiter = new RateLimiterRedis({
|
|
75
|
+
storeClient: redisClient,
|
|
76
|
+
keyPrefix: 'rl:key',
|
|
77
|
+
points: 500, // max tier limit — actual check done per-tier
|
|
78
|
+
duration,
|
|
79
|
+
blockDuration: 0, // don't block, just check
|
|
80
|
+
});
|
|
81
|
+
// IP-based limiter (global per-IP cap, on top of key limits)
|
|
82
|
+
this.ipLimiter = new RateLimiterRedis({
|
|
83
|
+
storeClient: redisClient,
|
|
84
|
+
keyPrefix: 'rl:ip',
|
|
85
|
+
points: GLOBAL_IP_LIMIT,
|
|
86
|
+
duration,
|
|
87
|
+
blockDuration: 0,
|
|
88
|
+
});
|
|
89
|
+
console.log('[rate-limit] Redis-backed rate limiting active (shared across pods)');
|
|
90
|
+
this.initialized = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.warn('[rate-limit] Failed to init Redis rate limiter, falling back to in-memory:', err.message);
|
|
96
|
+
}
|
|
97
|
+
// Fallback: in-memory
|
|
98
|
+
const { RateLimiterMemory } = await import('rate-limiter-flexible');
|
|
99
|
+
this.keyLimiter = new RateLimiterMemory({
|
|
100
|
+
keyPrefix: 'rl:key',
|
|
101
|
+
points: 500,
|
|
102
|
+
duration: Math.floor(this.windowMs / 1000),
|
|
103
|
+
});
|
|
104
|
+
this.ipLimiter = new RateLimiterMemory({
|
|
105
|
+
keyPrefix: 'rl:ip',
|
|
106
|
+
points: GLOBAL_IP_LIMIT,
|
|
107
|
+
duration: Math.floor(this.windowMs / 1000),
|
|
108
|
+
});
|
|
109
|
+
console.log('[rate-limit] In-memory rate limiting active (single-pod only)');
|
|
110
|
+
this.initialized = true;
|
|
111
|
+
}
|
|
112
|
+
async waitForInit() {
|
|
113
|
+
if (!this.initialized)
|
|
114
|
+
await this.initPromise;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if request is allowed.
|
|
118
|
+
* Returns { allowed, remaining, retryAfter? }
|
|
119
|
+
*/
|
|
120
|
+
async checkLimit(identifier, limit, cost = 1) {
|
|
121
|
+
await this.waitForInit();
|
|
122
|
+
try {
|
|
123
|
+
const res = await this.keyLimiter.consume(identifier, cost);
|
|
124
|
+
// rate-limiter-flexible tracks points consumed; we need to check against the per-tier limit
|
|
125
|
+
const consumed = res.consumedPoints;
|
|
126
|
+
if (consumed > limit) {
|
|
127
|
+
// Over the tier limit — reject
|
|
128
|
+
const msBeforeNext = res.msBeforeNext;
|
|
129
|
+
return {
|
|
130
|
+
allowed: false,
|
|
131
|
+
remaining: 0,
|
|
132
|
+
retryAfter: Math.ceil(msBeforeNext / 1000),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
allowed: true,
|
|
137
|
+
remaining: Math.max(0, limit - consumed),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (rejRes) {
|
|
141
|
+
// RateLimiterRes when rate limited
|
|
142
|
+
if (rejRes && rejRes.msBeforeNext !== undefined) {
|
|
143
|
+
return {
|
|
144
|
+
allowed: false,
|
|
145
|
+
remaining: 0,
|
|
146
|
+
retryAfter: Math.ceil(rejRes.msBeforeNext / 1000),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Unexpected error — allow through (fail-open)
|
|
150
|
+
console.error('[rate-limit] Error checking rate limit:', rejRes);
|
|
151
|
+
return { allowed: true, remaining: limit };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check per-IP limit (global cap regardless of API key).
|
|
156
|
+
*/
|
|
157
|
+
async checkIpLimit(ip, cost = 1) {
|
|
158
|
+
await this.waitForInit();
|
|
159
|
+
try {
|
|
160
|
+
const res = await this.ipLimiter.consume(ip, cost);
|
|
161
|
+
return {
|
|
162
|
+
allowed: true,
|
|
163
|
+
remaining: Math.max(0, GLOBAL_IP_LIMIT - res.consumedPoints),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (rejRes) {
|
|
167
|
+
if (rejRes && rejRes.msBeforeNext !== undefined) {
|
|
168
|
+
return {
|
|
169
|
+
allowed: false,
|
|
170
|
+
remaining: 0,
|
|
171
|
+
retryAfter: Math.ceil(rejRes.msBeforeNext / 1000),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { allowed: true, remaining: GLOBAL_IP_LIMIT };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Backward compat: cleanup is no-op for Redis (TTL handles it).
|
|
179
|
+
* Kept for in-memory fallback and for app.ts which calls it on an interval.
|
|
180
|
+
*/
|
|
181
|
+
cleanup() {
|
|
182
|
+
// No-op for Redis; RateLimiterMemory handles its own cleanup
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
186
|
+
export function createRateLimitMiddleware(limiter) {
|
|
187
|
+
return async (req, res, next) => {
|
|
188
|
+
try {
|
|
189
|
+
// Skip rate limiting for exempt endpoints
|
|
190
|
+
if (EXEMPT_PATHS.some(p => req.path === p || req.path.startsWith(p + '/'))) {
|
|
191
|
+
return next();
|
|
192
|
+
}
|
|
193
|
+
// Resolve real client IP (Cloudflare → x-forwarded-for → x-real-ip → req.ip)
|
|
194
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
195
|
+
const firstForwardedIp = typeof forwardedFor === 'string'
|
|
196
|
+
? forwardedFor.split(',')[0].trim()
|
|
197
|
+
: Array.isArray(forwardedFor) ? forwardedFor[0] : undefined;
|
|
198
|
+
const clientIp = req.headers['cf-connecting-ip']
|
|
199
|
+
|| firstForwardedIp
|
|
200
|
+
|| req.headers['x-real-ip']
|
|
201
|
+
|| req.ip
|
|
202
|
+
|| 'unknown';
|
|
203
|
+
// Key-based identifier: prefer API key, fall back to IP
|
|
204
|
+
const keyIdentifier = req.auth?.keyInfo?.key || `ip:${clientIp}`;
|
|
205
|
+
// Tier-based limit
|
|
206
|
+
const tier = req.auth?.tier || 'free';
|
|
207
|
+
const limit = TIER_BURST_LIMITS[tier] || 50;
|
|
208
|
+
// Route-based cost weighting
|
|
209
|
+
let cost = 1;
|
|
210
|
+
const path = req.path;
|
|
211
|
+
if (path.includes('/crawl') || path.includes('/map'))
|
|
212
|
+
cost = 5;
|
|
213
|
+
else if (path.includes('/batch'))
|
|
214
|
+
cost = 2;
|
|
215
|
+
else if (path.includes('/screenshot'))
|
|
216
|
+
cost = 2;
|
|
217
|
+
else if (req.query.render === 'true' || req.body?.render === true)
|
|
218
|
+
cost = 3;
|
|
219
|
+
// Check 1: Per-key rate limit
|
|
220
|
+
const keyResult = await limiter.checkLimit(keyIdentifier, limit, cost);
|
|
221
|
+
// Check 2: Per-IP rate limit (on top of key limit, prevents shared-key abuse)
|
|
222
|
+
const ipResult = await limiter.checkIpLimit(clientIp, cost);
|
|
223
|
+
// Use the more restrictive result
|
|
224
|
+
const allowed = keyResult.allowed && ipResult.allowed;
|
|
225
|
+
const remaining = Math.min(keyResult.remaining, ipResult.remaining);
|
|
226
|
+
const retryAfter = !allowed
|
|
227
|
+
? Math.max(keyResult.retryAfter || 0, ipResult.retryAfter || 0)
|
|
228
|
+
: undefined;
|
|
229
|
+
// Set rate limit headers
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
const resetTimestamp = Math.ceil((now + limiter.windowMs) / 1000);
|
|
232
|
+
res.setHeader('X-RateLimit-Limit', limit.toString());
|
|
233
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining).toString());
|
|
234
|
+
res.setHeader('X-RateLimit-Reset', resetTimestamp.toString());
|
|
235
|
+
if (req.auth?.tier) {
|
|
236
|
+
res.setHeader('X-WebPeel-Plan', req.auth.tier);
|
|
237
|
+
}
|
|
238
|
+
if (!allowed) {
|
|
239
|
+
const retryAfterSecs = retryAfter || 60;
|
|
240
|
+
res.setHeader('Retry-After', retryAfterSecs.toString());
|
|
241
|
+
const upgradeHint = tier === 'free'
|
|
242
|
+
? ' Upgrade to Pro ($9/mo) for 100/hr burst limit → https://webpeel.dev/pricing'
|
|
243
|
+
: tier === 'pro'
|
|
244
|
+
? ' Upgrade to Max ($29/mo) for 500/hr burst limit → https://webpeel.dev/pricing'
|
|
245
|
+
: '';
|
|
246
|
+
// Determine which limit was hit
|
|
247
|
+
const reason = !keyResult.allowed
|
|
248
|
+
? `Hourly rate limit exceeded (${limit} requests/hr on ${tier} plan)`
|
|
249
|
+
: `Global IP rate limit exceeded (${GLOBAL_IP_LIMIT} requests/hr)`;
|
|
250
|
+
res.status(429).json({
|
|
251
|
+
success: false,
|
|
252
|
+
error: {
|
|
253
|
+
type: 'rate_limited',
|
|
254
|
+
message: `${reason}. Try again in ${retryAfterSecs}s.`,
|
|
255
|
+
hint: `Retry after ${retryAfterSecs} seconds.${upgradeHint}`,
|
|
256
|
+
docs: 'https://webpeel.dev/docs/errors#rate-limited',
|
|
257
|
+
},
|
|
258
|
+
metadata: { requestId: req.requestId },
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
next();
|
|
263
|
+
}
|
|
264
|
+
catch (_error) {
|
|
265
|
+
// Fail-open: if rate limiter errors, allow the request through
|
|
266
|
+
console.error('[rate-limit] Middleware error, failing open:', _error);
|
|
267
|
+
next();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope enforcement middleware for API key permission scoping.
|
|
3
|
+
*
|
|
4
|
+
* Keys have one of three scopes:
|
|
5
|
+
* 'full' — all endpoints (default)
|
|
6
|
+
* 'read' — read/fetch operations only
|
|
7
|
+
* 'restricted' — /v1/scrape only (for limited sharing)
|
|
8
|
+
*
|
|
9
|
+
* JWT-authenticated requests (dashboard sessions) bypass scope enforcement:
|
|
10
|
+
* req.keyScope is undefined for JWT requests, which are always allowed through.
|
|
11
|
+
*/
|
|
12
|
+
import { Request, Response, NextFunction } from 'express';
|
|
13
|
+
import { KeyScope } from '../pg-auth-store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Middleware factory that enforces API key scope.
|
|
16
|
+
* Pass the set of scopes that are permitted to access the guarded route.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Only full-access keys may manage billing:
|
|
20
|
+
* router.post('/v1/billing', requireScope('full'), handler);
|
|
21
|
+
*
|
|
22
|
+
* // Read and full keys may scrape:
|
|
23
|
+
* app.use('/v1/scrape', requireScope('full', 'read', 'restricted'), scrapeRouter);
|
|
24
|
+
*/
|
|
25
|
+
export declare function requireScope(...allowedScopes: KeyScope[]): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope enforcement middleware for API key permission scoping.
|
|
3
|
+
*
|
|
4
|
+
* Keys have one of three scopes:
|
|
5
|
+
* 'full' — all endpoints (default)
|
|
6
|
+
* 'read' — read/fetch operations only
|
|
7
|
+
* 'restricted' — /v1/scrape only (for limited sharing)
|
|
8
|
+
*
|
|
9
|
+
* JWT-authenticated requests (dashboard sessions) bypass scope enforcement:
|
|
10
|
+
* req.keyScope is undefined for JWT requests, which are always allowed through.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Middleware factory that enforces API key scope.
|
|
14
|
+
* Pass the set of scopes that are permitted to access the guarded route.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Only full-access keys may manage billing:
|
|
18
|
+
* router.post('/v1/billing', requireScope('full'), handler);
|
|
19
|
+
*
|
|
20
|
+
* // Read and full keys may scrape:
|
|
21
|
+
* app.use('/v1/scrape', requireScope('full', 'read', 'restricted'), scrapeRouter);
|
|
22
|
+
*/
|
|
23
|
+
export function requireScope(...allowedScopes) {
|
|
24
|
+
return (req, res, next) => {
|
|
25
|
+
// JWT sessions (req.keyScope === undefined) always pass through.
|
|
26
|
+
// Scope enforcement only applies to API key requests.
|
|
27
|
+
if (req.keyScope === undefined) {
|
|
28
|
+
return next();
|
|
29
|
+
}
|
|
30
|
+
if (!allowedScopes.includes(req.keyScope)) {
|
|
31
|
+
res.status(403).json({
|
|
32
|
+
success: false,
|
|
33
|
+
error: {
|
|
34
|
+
type: 'insufficient_scope',
|
|
35
|
+
message: `This API key has '${req.keyScope}' scope. This endpoint requires: ${allowedScopes.join(' or ')}.`,
|
|
36
|
+
docs: 'https://webpeel.dev/docs/authentication#scopes',
|
|
37
|
+
hint: 'Create a new API key with the required scope in your dashboard.',
|
|
38
|
+
},
|
|
39
|
+
requestId: req.requestId,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation middleware to prevent SSRF attacks
|
|
3
|
+
* Validates URLs BEFORE any network request is made
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate URL to prevent SSRF attacks
|
|
7
|
+
* Blocks localhost, private IPs, link-local addresses, and non-HTTP(S) protocols
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateUrlForSSRF(urlString: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* SSRF Error class
|
|
12
|
+
*/
|
|
13
|
+
export declare class SSRFError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation middleware to prevent SSRF attacks
|
|
3
|
+
* Validates URLs BEFORE any network request is made
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate URL to prevent SSRF attacks
|
|
7
|
+
* Blocks localhost, private IPs, link-local addresses, and non-HTTP(S) protocols
|
|
8
|
+
*/
|
|
9
|
+
export function validateUrlForSSRF(urlString) {
|
|
10
|
+
// Parse URL
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(urlString);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error('Invalid URL format');
|
|
17
|
+
}
|
|
18
|
+
// Only allow HTTP(S)
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
20
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
21
|
+
}
|
|
22
|
+
const hostname = url.hostname.toLowerCase();
|
|
23
|
+
// Block localhost patterns
|
|
24
|
+
const localhostPatterns = ['localhost', '0.0.0.0'];
|
|
25
|
+
if (localhostPatterns.some(pattern => hostname === pattern || hostname.endsWith('.' + pattern))) {
|
|
26
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
27
|
+
}
|
|
28
|
+
// SECURITY: Block well-known cloud metadata service hostnames.
|
|
29
|
+
// These hostnames resolve to link-local IPs (169.254.x.x) which are blocked
|
|
30
|
+
// by IP, but hostname-level blocking provides defense-in-depth against DNS
|
|
31
|
+
// rebinding attacks where a domain transiently resolves to a valid IP during
|
|
32
|
+
// validation, then resolves to a private IP for the actual fetch.
|
|
33
|
+
const metadataHostnames = [
|
|
34
|
+
'metadata.google.internal', // GCP: resolves to 169.254.169.254
|
|
35
|
+
'metadata.goog', // GCP alternate
|
|
36
|
+
'metadata.internal', // Generic internal
|
|
37
|
+
'instance-data.ec2.internal', // AWS alternate
|
|
38
|
+
'computeMetadata', // Partial GCP hostname
|
|
39
|
+
];
|
|
40
|
+
if (metadataHostnames.some(m => hostname === m || hostname.endsWith('.' + m))) {
|
|
41
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
42
|
+
}
|
|
43
|
+
// Parse and validate IP addresses
|
|
44
|
+
const ipv4Info = parseIPv4(hostname);
|
|
45
|
+
if (ipv4Info) {
|
|
46
|
+
validateIPv4ForSSRF(ipv4Info);
|
|
47
|
+
}
|
|
48
|
+
// Validate IPv6
|
|
49
|
+
if (hostname.includes(':')) {
|
|
50
|
+
validateIPv6ForSSRF(hostname);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* SSRF Error class
|
|
55
|
+
*/
|
|
56
|
+
export class SSRFError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'SSRFError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse IPv4 address in any format (dotted, hex, octal, decimal)
|
|
64
|
+
*/
|
|
65
|
+
function parseIPv4(hostname) {
|
|
66
|
+
const cleaned = hostname.replace(/^\[|\]$/g, '');
|
|
67
|
+
// Standard dotted notation: 192.168.1.1
|
|
68
|
+
const dottedRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
69
|
+
const dottedMatch = cleaned.match(dottedRegex);
|
|
70
|
+
if (dottedMatch) {
|
|
71
|
+
const octets = dottedMatch.slice(1).map(Number);
|
|
72
|
+
if (octets.every(o => o >= 0 && o <= 255)) {
|
|
73
|
+
return octets;
|
|
74
|
+
}
|
|
75
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
76
|
+
}
|
|
77
|
+
// Hex notation: 0x7f000001
|
|
78
|
+
if (/^0x[0-9a-fA-F]+$/.test(cleaned)) {
|
|
79
|
+
const num = parseInt(cleaned, 16);
|
|
80
|
+
return [
|
|
81
|
+
(num >>> 24) & 0xff,
|
|
82
|
+
(num >>> 16) & 0xff,
|
|
83
|
+
(num >>> 8) & 0xff,
|
|
84
|
+
num & 0xff,
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
// Octal notation
|
|
88
|
+
if (/^0[0-7]/.test(cleaned)) {
|
|
89
|
+
if (/^0[0-7]+$/.test(cleaned)) {
|
|
90
|
+
const num = parseInt(cleaned, 8);
|
|
91
|
+
if (num <= 0xffffffff) {
|
|
92
|
+
return [
|
|
93
|
+
(num >>> 24) & 0xff,
|
|
94
|
+
(num >>> 16) & 0xff,
|
|
95
|
+
(num >>> 8) & 0xff,
|
|
96
|
+
num & 0xff,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const parts = cleaned.split('.');
|
|
101
|
+
if (parts.length === 4) {
|
|
102
|
+
const octets = parts.map(p => parseInt(p, /^0[0-7]/.test(p) ? 8 : 10));
|
|
103
|
+
if (octets.every(o => o >= 0 && o <= 255)) {
|
|
104
|
+
return octets;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Decimal notation: 2130706433
|
|
109
|
+
if (/^\d+$/.test(cleaned)) {
|
|
110
|
+
const num = parseInt(cleaned, 10);
|
|
111
|
+
if (num <= 0xffffffff) {
|
|
112
|
+
return [
|
|
113
|
+
(num >>> 24) & 0xff,
|
|
114
|
+
(num >>> 16) & 0xff,
|
|
115
|
+
(num >>> 8) & 0xff,
|
|
116
|
+
num & 0xff,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validate IPv4 address against private/reserved ranges
|
|
124
|
+
*/
|
|
125
|
+
function validateIPv4ForSSRF(octets) {
|
|
126
|
+
const [a, b, c, d] = octets;
|
|
127
|
+
// Loopback: 127.0.0.0/8
|
|
128
|
+
if (a === 127) {
|
|
129
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
130
|
+
}
|
|
131
|
+
// Private: 10.0.0.0/8
|
|
132
|
+
if (a === 10) {
|
|
133
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
134
|
+
}
|
|
135
|
+
// Private: 172.16.0.0/12
|
|
136
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
137
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
138
|
+
}
|
|
139
|
+
// Private: 192.168.0.0/16
|
|
140
|
+
if (a === 192 && b === 168) {
|
|
141
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
142
|
+
}
|
|
143
|
+
// Link-local: 169.254.0.0/16 (includes AWS metadata endpoint)
|
|
144
|
+
if (a === 169 && b === 254) {
|
|
145
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
146
|
+
}
|
|
147
|
+
// Broadcast: 255.255.255.255
|
|
148
|
+
if (a === 255 && b === 255 && c === 255 && d === 255) {
|
|
149
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
150
|
+
}
|
|
151
|
+
// This network: 0.0.0.0/8
|
|
152
|
+
if (a === 0) {
|
|
153
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Validate IPv6 address against private/reserved ranges
|
|
158
|
+
*/
|
|
159
|
+
function validateIPv6ForSSRF(hostname) {
|
|
160
|
+
const addr = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
161
|
+
// Loopback: ::1
|
|
162
|
+
if (addr === '::1' || addr === '0:0:0:0:0:0:0:1') {
|
|
163
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
164
|
+
}
|
|
165
|
+
// IPv6 mapped IPv4: ::ffff:192.168.1.1
|
|
166
|
+
if (addr.startsWith('::ffff:')) {
|
|
167
|
+
const ipv4Part = addr.substring(7);
|
|
168
|
+
if (ipv4Part.includes('.')) {
|
|
169
|
+
const parts = ipv4Part.split('.');
|
|
170
|
+
if (parts.length === 4) {
|
|
171
|
+
const octets = parts.map(p => parseInt(p, 10));
|
|
172
|
+
if (octets.every(o => !isNaN(o) && o >= 0 && o <= 255)) {
|
|
173
|
+
validateIPv4ForSSRF(octets);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const hexStr = ipv4Part.replace(/:/g, '');
|
|
179
|
+
if (/^[0-9a-f]{1,8}$/.test(hexStr)) {
|
|
180
|
+
const num = parseInt(hexStr, 16);
|
|
181
|
+
const octets = [
|
|
182
|
+
(num >>> 24) & 0xff,
|
|
183
|
+
(num >>> 16) & 0xff,
|
|
184
|
+
(num >>> 8) & 0xff,
|
|
185
|
+
num & 0xff,
|
|
186
|
+
];
|
|
187
|
+
validateIPv4ForSSRF(octets);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
191
|
+
}
|
|
192
|
+
// Unique local addresses: fc00::/7
|
|
193
|
+
if (addr.startsWith('fc') || addr.startsWith('fd')) {
|
|
194
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
195
|
+
}
|
|
196
|
+
// Link-local: fe80::/10
|
|
197
|
+
if (addr.startsWith('fe8') || addr.startsWith('fe9') ||
|
|
198
|
+
addr.startsWith('fea') || addr.startsWith('feb')) {
|
|
199
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
200
|
+
}
|
|
201
|
+
}
|