Skip to content

Commit ee18b4c

Browse files
committed
chore: updates
1 parent d69e57a commit ee18b4c

1 file changed

Lines changed: 39 additions & 4 deletions

File tree

src/index.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,19 @@ const server = new FastMCP<SessionData>({
304304
305305
if (process.env.CLOUD_SERVICE === 'true') {
306306
if (!headerCred) {
307-
// Keyless free tier over the hosted MCP: allowed only when a forwarding
308-
// secret is configured AND we can determine the end-user's client IP, so
309-
// the API rate-limits per real client IP rather than the shared server IP.
307+
// Keyless free tier over the hosted MCP: serve it only when a forwarding
308+
// secret is configured, we know the end-user's client IP (so the API can
309+
// rate-limit per real IP, not the shared server IP), AND that IP still
310+
// has free quota. If the IP is out of quota (or keyless is off), fall
311+
// through to throw so FastMCP emits the OAuth 401 + WWW-Authenticate
312+
// challenge — i.e. prompt the user to connect an account exactly when
313+
// their free quota runs out.
310314
const clientIp = extractClientIp(request);
311-
if (process.env.KEYLESS_PROXY_SECRET && clientIp) {
315+
if (
316+
process.env.KEYLESS_PROXY_SECRET &&
317+
clientIp &&
318+
(await keylessEligible(clientIp))
319+
) {
312320
return { firecrawlApiKey: undefined, research, keylessClientIp: clientIp };
313321
}
314322
throw new Error(
@@ -961,6 +969,33 @@ function extractClientIp(request?: {
961969
return first || undefined;
962970
}
963971

972+
/**
973+
* Read-only check (no quota consumed) of whether a client IP can still use the
974+
* keyless free tier, via the API's secret-gated eligibility endpoint. Fails
975+
* closed: anything other than a clear "eligible: true" means fall through to the
976+
* OAuth challenge rather than silently granting keyless.
977+
*/
978+
async function keylessEligible(clientIp: string): Promise<boolean> {
979+
const secret = process.env.KEYLESS_PROXY_SECRET;
980+
if (!secret) return false;
981+
try {
982+
const response = await fetch(
983+
`${resolveApiBaseUrl()}/v2/keyless/eligibility`,
984+
{
985+
headers: {
986+
'x-firecrawl-keyless-ip': clientIp,
987+
'x-firecrawl-keyless-secret': secret,
988+
},
989+
}
990+
);
991+
if (!response.ok) return false;
992+
const json: any = await response.json().catch(() => ({}));
993+
return json?.eligible === true;
994+
} catch {
995+
return false;
996+
}
997+
}
998+
964999
function isKeylessMode(session?: SessionData): boolean {
9651000
if (session?.firecrawlApiKey) return false;
9661001
if (process.env.CLOUD_SERVICE === 'true') {

0 commit comments

Comments
 (0)