* Keyless free tier: official MCP/CLI/SDK clients can call scrape, search, and * interact with no API key. `origin`/`integration` are client-set and spoofable, * so they're only a soft UX gate — the real abuse controls are the per-IP daily * request + credit caps plus the `keyless/consume` canoni
( req, mode: RateLimiterMode | undefined, allowKeyless: boolean | undefined, )
| 484 | * Always returns an AuthResponse — handles every no-API-key request. |
| 485 | */ |
| 486 | async function handleKeylessAuth( |
| 487 | req, |
| 488 | mode: RateLimiterMode | undefined, |
| 489 | allowKeyless: boolean | undefined, |
| 490 | ): Promise<AuthResponse> { |
| 491 | const unauthorized: AuthResponse = { |
| 492 | success: false, |
| 493 | error: "Unauthorized", |
| 494 | status: 401, |
| 495 | }; |
| 496 | |
| 497 | // The keyless tier is off unless BOTH limits are configured (even to 0). When |
| 498 | // unconfigured we behave exactly as before — a generic 401 — and don't reveal |
| 499 | // that the tier exists. |
| 500 | if (!isKeylessConfigured()) return unauthorized; |
| 501 | |
| 502 | // Configured, but this endpoint isn't part of the keyless tier: tell the user |
| 503 | // they need a key (with the signup nudge) rather than a bare "Unauthorized". |
| 504 | if (!allowKeyless) { |
| 505 | return { |
| 506 | success: false, |
| 507 | error: KEYLESS_ENDPOINT_NOT_AVAILABLE_MESSAGE, |
| 508 | status: 401, |
| 509 | }; |
| 510 | } |
| 511 | |
| 512 | const origin = req.body?.origin; |
| 513 | const integration = req.body?.integration; |
| 514 | // No origin/surface gate: any request without an API key may use the free |
| 515 | // tier on the allowlisted endpoints (the API itself is free). origin and |
| 516 | // integration are still recorded below for abuse monitoring. |
| 517 | |
| 518 | // Key on the real client IP. A trusted proxy (e.g. the hosted MCP) may |
| 519 | // forward the end-user's IP via x-firecrawl-keyless-ip, authenticated with a |
| 520 | // shared secret — without the secret the header is ignored, so direct callers |
| 521 | // can't spoof their IP to dodge the per-IP cap. |
| 522 | let ip = req.ip ?? req.socket?.remoteAddress ?? "unknown"; |
| 523 | if ( |
| 524 | config.KEYLESS_PROXY_SECRET && |
| 525 | req.headers["x-firecrawl-keyless-secret"] === config.KEYLESS_PROXY_SECRET |
| 526 | ) { |
| 527 | const forwarded = req.headers["x-firecrawl-keyless-ip"]; |
| 528 | if (typeof forwarded === "string" && forwarded.trim()) { |
| 529 | ip = forwarded.trim(); |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | // Only a valid IPv4 identity gets keyless: IPv6 is too cheap to rotate for a |
| 534 | // per-IP cap to mean anything, and malformed/forwarded values must not be |
| 535 | // usable as arbitrary limiter buckets. Anything else falls through to 401. |
| 536 | if (!isKeylessIpEligible(ip)) return unauthorized; |
| 537 | |
| 538 | // Optional Spur Context check (only when SPUR_API_KEY is set): refuse keyless |
| 539 | // for IPs fronting anonymizing/rotating infrastructure (VPN/proxy/TOR), the |
| 540 | // main way the per-IP caps get bypassed. Fails open on any Spur error, and |
| 541 | // runs before consuming quota so a flagged IP doesn't burn a request slot. |
| 542 | if (await isKeylessIpSuspicious(ip)) { |
| 543 | logger.warn("Keyless request blocked: suspicious IP", { |
no test coverage detected
searching dependent graphs…