MCPcopy
hub / github.com/firecrawl/firecrawl / handleKeylessAuth

Function handleKeylessAuth

apps/api/src/controllers/auth.ts:486–628  ·  view source on GitHub ↗

* 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,
)

Source from the content-addressed store, hash-verified

484 * Always returns an AuthResponse — handles every no-API-key request.
485 */
486async 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", {

Callers 1

supaAuthenticateUserFunction · 0.85

Calls 6

isKeylessConfiguredFunction · 0.90
isKeylessIpEligibleFunction · 0.90
isKeylessIpSuspiciousFunction · 0.90
keylessTeamIdFunction · 0.90
consumeKeylessRequestFunction · 0.90
mockPreviewACUCFunction · 0.85

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…