2024-08-26 18:48:00 -03:00
import { parseApi } from "../lib/parseApi" ;
2025-01-30 13:47:29 -03:00
import { getRateLimiter , isTestSuiteToken } from "../services/rate-limiter" ;
2024-08-12 13:42:09 -04:00
import {
AuthResponse ,
NotificationType ,
2024-12-11 19:51:08 -03:00
RateLimiterMode ,
2024-08-26 18:48:00 -03:00
} from "../types" ;
2025-03-03 20:53:51 +01:00
import { supabase_rr_service , supabase_service } from "../services/supabase" ;
2024-08-26 18:48:00 -03:00
import { withAuth } from "../lib/withAuth" ;
2024-05-14 18:08:31 -03:00
import { RateLimiterRedis } from "rate-limiter-flexible" ;
2024-08-26 18:48:00 -03:00
import { sendNotification } from "../services/notification/email_notification" ;
2024-11-07 20:57:33 +01:00
import { logger } from "../lib/logger" ;
2024-08-26 18:48:00 -03:00
import { redlock } from "../services/redlock" ;
2024-10-22 20:28:10 +02:00
import { deleteKey , getValue } from "../services/redis" ;
2024-08-26 18:48:00 -03:00
import { setValue } from "../services/redis" ;
2024-08-12 13:42:09 -04:00
import { validate } from "uuid" ;
2024-08-22 03:55:40 +02:00
import * as Sentry from "@sentry/node" ;
2025-04-10 18:49:23 +02:00
import { AuthCreditUsageChunk , AuthCreditUsageChunkFromTeam } from "./v1/types" ;
2024-08-26 19:57:27 -03:00
// const { data, error } = await supabase_service
// .from('api_keys')
// .select(`
// key,
// team_id,
// teams (
// subscriptions (
// price_id
// )
// )
// `)
// .eq('key', normalizedApi)
// .limit(1)
// .single();
2024-08-12 13:37:47 -04:00
function normalizedApiIsUuid ( potentialUuid : string ) : boolean {
// Check if the string is a valid UUID
return validate ( potentialUuid ) ;
}
2024-09-25 19:25:18 +02:00
2024-10-14 12:24:34 -03:00
export async function setCachedACUC (
api_key : string ,
2025-04-10 18:49:23 +02:00
is_extract : boolean ,
2024-10-14 12:24:34 -03:00
acuc :
2024-12-11 19:46:11 -03:00
| AuthCreditUsageChunk
| null
2024-12-11 19:51:08 -03:00
| ( ( acuc : AuthCreditUsageChunk ) = > AuthCreditUsageChunk | null ) ,
2024-10-14 12:24:34 -03:00
) {
2025-04-10 18:49:23 +02:00
const cacheKeyACUC = ` acuc_ ${ api_key } _ ${ is_extract ? "extract" : "scrape" } ` ;
2024-09-25 19:25:18 +02:00
const redLockKey = ` lock_ ${ cacheKeyACUC } ` ;
try {
2024-10-14 12:24:34 -03:00
await redlock . using ( [ redLockKey ] , 10000 , { } , async ( signal ) = > {
2024-09-25 22:15:02 +02:00
if ( typeof acuc === "function" ) {
2024-12-11 19:46:11 -03:00
acuc = acuc ( JSON . parse ( ( await getValue ( cacheKeyACUC ) ) ? ? "null" ) ) ;
2024-09-25 22:15:02 +02:00
if ( acuc === null ) {
2024-09-25 22:47:42 +02:00
if ( signal . aborted ) {
throw signal . error ;
}
2024-09-25 22:15:02 +02:00
return ;
}
}
2024-09-25 22:47:42 +02:00
if ( signal . aborted ) {
throw signal . error ;
}
2025-03-03 16:26:42 -03:00
// Cache for 1 hour. - mogery
await setValue ( cacheKeyACUC , JSON . stringify ( acuc ) , 3600 , true ) ;
2024-09-25 22:47:42 +02:00
} ) ;
2024-09-25 19:25:18 +02:00
} catch ( error ) {
2024-11-07 20:57:33 +01:00
logger . error ( ` Error updating cached ACUC ${ cacheKeyACUC } : ${ error } ` ) ;
2024-09-25 19:25:18 +02:00
}
}
2025-04-13 23:21:34 -07:00
const mockPreviewACUC : ( team_id : string , is_extract : boolean ) = > AuthCreditUsageChunk = ( team_id , is_extract ) = > ( {
2025-04-13 23:16:55 -07:00
api_key : "preview" ,
team_id ,
sub_id : "bypass" ,
sub_current_period_start : new Date ( ) . toISOString ( ) ,
sub_current_period_end : new Date ( new Date ( ) . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ,
sub_user_id : "bypass" ,
price_id : "bypass" ,
rate_limits : {
crawl : 2 ,
scrape : 10 ,
extract : 10 ,
search : 5 ,
map : 5 ,
preview : 5 ,
crawlStatus : 500 ,
extractStatus : 500 ,
} ,
price_credits : 99999999 ,
credits_used : 0 ,
coupon_credits : 99999999 ,
adjusted_credits_used : 0 ,
remaining_credits : 99999999 ,
total_credits_sum : 99999999 ,
plan_priority : {
bucketLimit : 25 ,
planModifier : 0.1 ,
} ,
2025-04-13 23:21:34 -07:00
concurrency : is_extract ? 200 : 2 ,
is_extract ,
2025-04-13 23:16:55 -07:00
} ) ;
2025-04-10 18:49:23 +02:00
const mockACUC : ( ) = > AuthCreditUsageChunk = ( ) = > ( {
api_key : "bypass" ,
team_id : "bypass" ,
sub_id : "bypass" ,
sub_current_period_start : new Date ( ) . toISOString ( ) ,
sub_current_period_end : new Date ( new Date ( ) . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ,
sub_user_id : "bypass" ,
price_id : "bypass" ,
rate_limits : {
crawl : 99999999 ,
scrape : 99999999 ,
extract : 99999999 ,
search : 99999999 ,
map : 99999999 ,
preview : 99999999 ,
crawlStatus : 99999999 ,
extractStatus : 99999999 ,
} ,
price_credits : 99999999 ,
credits_used : 0 ,
coupon_credits : 99999999 ,
adjusted_credits_used : 0 ,
remaining_credits : 99999999 ,
total_credits_sum : 99999999 ,
plan_priority : {
bucketLimit : 25 ,
planModifier : 0.1 ,
} ,
concurrency : 99999999 ,
is_extract : false ,
} ) ;
2024-10-14 12:24:34 -03:00
export async function getACUC (
api_key : string ,
2024-10-22 19:47:23 -03:00
cacheOnly = false ,
2024-12-11 19:51:08 -03:00
useCache = true ,
2025-01-19 22:04:12 -03:00
mode? : RateLimiterMode ,
2024-10-14 12:24:34 -03:00
) : Promise < AuthCreditUsageChunk | null > {
2025-04-10 18:49:23 +02:00
let isExtract =
mode === RateLimiterMode . Extract ||
mode === RateLimiterMode . ExtractStatus ;
2025-04-13 23:21:34 -07:00
if ( api_key === process . env . PREVIEW_TOKEN ) {
const acuc = mockPreviewACUC ( api_key , isExtract ) ;
acuc . is_extract = isExtract ;
return acuc ;
}
2025-04-10 18:49:23 +02:00
if ( process . env . USE_DB_AUTHENTICATION !== "true" ) {
const acuc = mockACUC ( ) ;
acuc . is_extract = isExtract ;
return acuc ;
}
const cacheKeyACUC = ` acuc_ ${ api_key } _ ${ isExtract ? "extract" : "scrape" } ` ;
2024-09-25 19:25:18 +02:00
2025-04-10 16:08:20 +02:00
// if (useCache) {
// const cachedACUC = await getValue(cacheKeyACUC);
// if (cachedACUC !== null) {
// return JSON.parse(cachedACUC);
// }
// }
// if (!cacheOnly) {
2024-10-14 12:24:34 -03:00
let data ;
let error ;
let retries = 0 ;
const maxRetries = 5 ;
2025-04-10 18:49:23 +02:00
while ( retries < maxRetries ) {
const client =
2025-04-11 07:13:25 +02:00
Math . random ( ) > ( 2 / 3 ) ? supabase_rr_service : supabase_service ;
2025-04-10 18:49:23 +02:00
( { data , error } = await client . rpc (
2025-04-13 10:32:03 -07:00
"auth_credit_usage_chunk_30" ,
2025-04-10 18:49:23 +02:00
{ input_key : api_key , i_is_extract : isExtract , tally_untallied_credits : true } ,
{ get : true } ,
) ) ;
if ( ! error ) {
break ;
}
logger . warn (
` Failed to retrieve authentication and credit usage data after ${ retries } , trying again... ` ,
{ error }
) ;
retries ++ ;
if ( retries === maxRetries ) {
throw new Error (
"Failed to retrieve authentication and credit usage data after 3 attempts: " +
JSON . stringify ( error ) ,
) ;
}
// Wait for a short time before retrying
await new Promise ( ( resolve ) = > setTimeout ( resolve , 200 ) ) ;
}
const chunk : AuthCreditUsageChunk | null =
data . length === 0 ? null : data [ 0 ] . team_id === null ? null : data [ 0 ] ;
// NOTE: Should we cache null chunks? - mogery
// if (chunk !== null && useCache) {
// setCachedACUC(api_key, isExtract, chunk);
// }
return chunk ? { . . . chunk , is_extract : isExtract } : null ;
// } else {
// return null;
// }
}
export async function setCachedACUCTeam (
team_id : string ,
is_extract : boolean ,
acuc :
| AuthCreditUsageChunkFromTeam
| null
| ( ( acuc : AuthCreditUsageChunkFromTeam ) = > AuthCreditUsageChunkFromTeam | null ) ,
) {
const cacheKeyACUC = ` acuc_team_ ${ team_id } _ ${ is_extract ? "extract" : "scrape" } ` ;
const redLockKey = ` lock_ ${ cacheKeyACUC } ` ;
try {
await redlock . using ( [ redLockKey ] , 10000 , { } , async ( signal ) = > {
if ( typeof acuc === "function" ) {
acuc = acuc ( JSON . parse ( ( await getValue ( cacheKeyACUC ) ) ? ? "null" ) ) ;
if ( acuc === null ) {
if ( signal . aborted ) {
throw signal . error ;
}
return ;
}
}
if ( signal . aborted ) {
throw signal . error ;
}
2024-10-14 12:24:34 -03:00
2025-04-10 18:49:23 +02:00
// Cache for 1 hour. - mogery
await setValue ( cacheKeyACUC , JSON . stringify ( acuc ) , 3600 , true ) ;
} ) ;
} catch ( error ) {
logger . error ( ` Error updating cached ACUC ${ cacheKeyACUC } : ${ error } ` ) ;
}
}
export async function getACUCTeam (
team_id : string ,
cacheOnly = false ,
useCache = true ,
mode? : RateLimiterMode ,
) : Promise < AuthCreditUsageChunkFromTeam | null > {
let isExtract =
2025-02-14 11:12:02 -03:00
mode === RateLimiterMode . Extract ||
mode === RateLimiterMode . ExtractStatus ;
2025-04-13 23:16:55 -07:00
if ( team_id . startsWith ( "preview" ) ) {
2025-04-13 23:21:34 -07:00
const acuc = mockPreviewACUC ( team_id , isExtract ) ;
2025-04-13 23:16:55 -07:00
return acuc ;
}
2025-04-10 18:49:23 +02:00
if ( process . env . USE_DB_AUTHENTICATION !== "true" ) {
const acuc = mockACUC ( ) ;
acuc . is_extract = isExtract ;
return acuc ;
}
const cacheKeyACUC = ` acuc_team_ ${ team_id } _ ${ isExtract ? "extract" : "scrape" } ` ;
// if (useCache) {
// const cachedACUC = await getValue(cacheKeyACUC);
// if (cachedACUC !== null) {
// return JSON.parse(cachedACUC);
// }
// }
// if (!cacheOnly) {
let data ;
let error ;
let retries = 0 ;
const maxRetries = 5 ;
2024-10-14 12:24:34 -03:00
while ( retries < maxRetries ) {
2025-04-02 10:45:11 -03:00
const client =
2025-04-11 07:13:25 +02:00
Math . random ( ) > ( 2 / 3 ) ? supabase_rr_service : supabase_service ;
2025-03-03 20:53:51 +01:00
( { data , error } = await client . rpc (
2025-04-13 10:32:03 -07:00
"auth_credit_usage_chunk_30_from_team" ,
2025-04-10 18:49:23 +02:00
{ input_team : team_id , i_is_extract : isExtract , tally_untallied_credits : true } ,
2024-12-11 19:51:08 -03:00
{ get : true } ,
2024-10-14 12:24:34 -03:00
) ) ;
if ( ! error ) {
break ;
}
2024-11-07 20:57:33 +01:00
logger . warn (
2024-12-11 19:51:08 -03:00
` Failed to retrieve authentication and credit usage data after ${ retries } , trying again... ` ,
2025-03-12 20:10:33 +01:00
{ error }
2024-10-14 12:24:34 -03:00
) ;
retries ++ ;
if ( retries === maxRetries ) {
throw new Error (
"Failed to retrieve authentication and credit usage data after 3 attempts: " +
2024-12-11 19:51:08 -03:00
JSON . stringify ( error ) ,
2024-10-14 12:24:34 -03:00
) ;
}
// Wait for a short time before retrying
await new Promise ( ( resolve ) = > setTimeout ( resolve , 200 ) ) ;
2024-09-25 19:25:18 +02:00
}
2024-10-14 12:24:34 -03:00
const chunk : AuthCreditUsageChunk | null =
data . length === 0 ? null : data [ 0 ] . team_id === null ? null : data [ 0 ] ;
2024-09-25 19:25:18 +02:00
// NOTE: Should we cache null chunks? - mogery
2025-04-10 16:08:20 +02:00
// if (chunk !== null && useCache) {
// setCachedACUC(api_key, chunk);
// }
2024-12-11 19:46:11 -03:00
2025-01-31 01:10:59 -03:00
return chunk ? { . . . chunk , is_extract : isExtract } : null ;
2025-04-10 16:08:20 +02:00
// } else {
// return null;
// }
2024-09-25 19:25:18 +02:00
}
2024-12-11 19:46:11 -03:00
export async function clearACUC ( api_key : string ) : Promise < void > {
2025-01-21 19:17:06 -03:00
// Delete cache for all rate limiter modes
2025-04-10 18:49:23 +02:00
const modes = [ true , false ] ;
2025-01-21 19:17:06 -03:00
await Promise . all (
modes . map ( async ( mode ) = > {
2025-04-10 18:49:23 +02:00
const cacheKey = ` acuc_ ${ api_key } _ ${ mode ? "extract" : "scrape" } ` ;
2025-01-21 19:17:06 -03:00
await deleteKey ( cacheKey ) ;
2025-01-22 18:47:44 -03:00
} ) ,
2025-01-21 19:17:06 -03:00
) ;
// Also clear the base cache key
await deleteKey ( ` acuc_ ${ api_key } ` ) ;
2024-10-22 20:28:10 +02:00
}
2025-04-10 18:49:23 +02:00
export async function clearACUCTeam ( team_id : string ) : Promise < void > {
// Delete cache for all rate limiter modes
const modes = [ true , false ] ;
await Promise . all (
modes . map ( async ( mode ) = > {
const cacheKey = ` acuc_team_ ${ team_id } _ ${ mode ? "extract" : "scrape" } ` ;
await deleteKey ( cacheKey ) ;
} ) ,
) ;
// Also clear the base cache key
await deleteKey ( ` acuc_team_ ${ team_id } ` ) ;
}
2024-08-12 13:42:09 -04:00
export async function authenticateUser (
req ,
res ,
2024-12-11 19:51:08 -03:00
mode? : RateLimiterMode ,
2024-08-12 13:42:09 -04:00
) : Promise < AuthResponse > {
2024-12-11 19:46:11 -03:00
return withAuth ( supaAuthenticateUser , {
success : true ,
chunk : null ,
2024-12-11 19:51:08 -03:00
team_id : "bypass" ,
2024-12-11 19:46:11 -03:00
} ) ( req , res , mode ) ;
2024-05-20 13:36:34 -07:00
}
2024-08-12 15:07:30 -04:00
2024-04-21 10:36:48 -07:00
export async function supaAuthenticateUser (
2024-04-20 16:38:05 -07:00
req ,
res ,
2024-12-11 19:51:08 -03:00
mode? : RateLimiterMode ,
2024-11-07 20:57:33 +01:00
) : Promise < AuthResponse > {
2024-10-14 12:24:34 -03:00
const authHeader =
req . headers . authorization ? ?
( req . headers [ "sec-websocket-protocol" ]
? ` Bearer ${ req . headers [ "sec-websocket-protocol" ] } `
: null ) ;
2024-04-20 16:38:05 -07:00
if ( ! authHeader ) {
return { success : false , error : "Unauthorized" , status : 401 } ;
}
const token = authHeader . split ( " " ) [ 1 ] ; // Extract the token from "Bearer <token>"
if ( ! token ) {
return {
success : false ,
error : "Unauthorized: Token missing" ,
2024-12-11 19:51:08 -03:00
status : 401 ,
2024-04-20 16:38:05 -07:00
} ;
}
2025-04-13 23:26:57 -07:00
const incomingIP = ( req . headers [ "x-preview-ip" ] || req . headers [ "x-forwarded-for" ] ||
2024-05-14 18:08:31 -03:00
req . socket . remoteAddress ) as string ;
const iptoken = incomingIP + token ;
let rateLimiter : RateLimiterRedis ;
2025-04-10 18:49:23 +02:00
let subscriptionData : { team_id : string } | null = null ;
2024-05-14 18:08:31 -03:00
let normalizedApi : string ;
2024-08-12 13:37:47 -04:00
let teamId : string | null = null ;
let priceId : string | null = null ;
2024-11-07 20:57:33 +01:00
let chunk : AuthCreditUsageChunk | null = null ;
2024-05-14 18:08:31 -03:00
if ( token == "this_is_just_a_preview_token" ) {
2025-03-06 19:03:33 -03:00
throw new Error (
"Unauthenticated Playground calls are temporarily disabled due to abuse. Please sign up." ,
) ;
}
if ( token == process . env . PREVIEW_TOKEN ) {
2024-08-28 14:18:05 -03:00
if ( mode == RateLimiterMode . CrawlStatus ) {
rateLimiter = getRateLimiter ( RateLimiterMode . CrawlStatus , token ) ;
2025-01-19 23:17:12 -03:00
} else if ( mode == RateLimiterMode . ExtractStatus ) {
rateLimiter = getRateLimiter ( RateLimiterMode . ExtractStatus , token ) ;
2024-08-28 14:18:05 -03:00
} else {
rateLimiter = getRateLimiter ( RateLimiterMode . Preview , token ) ;
2024-10-14 12:24:34 -03:00
}
2025-01-25 15:03:09 -03:00
teamId = ` preview_ ${ iptoken } ` ;
2024-05-17 15:37:47 -03:00
} else {
2024-05-14 18:08:31 -03:00
normalizedApi = parseApi ( token ) ;
2024-08-12 13:42:09 -04:00
if ( ! normalizedApiIsUuid ( normalizedApi ) ) {
2024-08-12 13:37:47 -04:00
return {
success : false ,
error : "Unauthorized: Invalid token" ,
2024-12-11 19:51:08 -03:00
status : 401 ,
2024-08-12 13:37:47 -04:00
} ;
}
2024-08-12 15:07:30 -04:00
2025-01-19 22:04:12 -03:00
chunk = await getACUC ( normalizedApi , false , true , mode ) ;
2025-01-10 18:35:10 -03:00
2024-09-25 19:25:18 +02:00
if ( chunk === null ) {
return {
success : false ,
error : "Unauthorized: Invalid token" ,
2024-12-11 19:51:08 -03:00
status : 401 ,
2024-09-25 19:25:18 +02:00
} ;
2024-08-12 13:37:47 -04:00
}
2024-05-14 18:08:31 -03:00
2024-09-25 19:25:18 +02:00
teamId = chunk . team_id ;
priceId = chunk . price_id ;
2024-05-14 18:08:31 -03:00
subscriptionData = {
2024-08-12 13:37:47 -04:00
team_id : teamId ,
2024-08-12 13:42:09 -04:00
} ;
2025-04-10 18:49:23 +02:00
rateLimiter = getRateLimiter (
mode ? ? RateLimiterMode . Crawl ,
chunk . rate_limits ,
) ;
2024-05-14 18:08:31 -03:00
}
2024-08-12 13:42:09 -04:00
const team_endpoint_token =
2025-03-06 19:03:33 -03:00
token === process . env . PREVIEW_TOKEN ? iptoken : teamId ;
2024-06-05 13:20:26 -07:00
2024-04-20 16:38:05 -07:00
try {
2024-06-05 13:20:26 -07:00
await rateLimiter . consume ( team_endpoint_token ) ;
2024-04-20 16:38:05 -07:00
} catch ( rateLimiterRes ) {
2024-12-11 19:46:11 -03:00
logger . error ( ` Rate limit exceeded: ${ rateLimiterRes } ` , {
teamId ,
priceId ,
mode ,
2025-04-10 18:49:23 +02:00
rateLimits : chunk?.rate_limits ,
2024-12-11 19:51:08 -03:00
rateLimiterRes ,
2024-12-11 19:46:11 -03:00
} ) ;
2024-05-25 11:57:49 -04:00
const secs = Math . round ( rateLimiterRes . msBeforeNext / 1000 ) || 1 ;
const retryDate = new Date ( Date . now ( ) + rateLimiterRes . msBeforeNext ) ;
2024-06-05 13:20:26 -07:00
// We can only send a rate limit email every 7 days, send notification already has the date in between checking
const startDate = new Date ( ) ;
const endDate = new Date ( ) ;
endDate . setDate ( endDate . getDate ( ) + 7 ) ;
2024-08-12 13:42:09 -04:00
2024-06-07 10:39:11 -07:00
// await sendNotification(team_id, NotificationType.RATE_LIMIT_REACHED, startDate.toISOString(), endDate.toISOString());
2024-08-12 13:37:47 -04:00
2024-04-20 16:38:05 -07:00
return {
success : false ,
2024-08-21 21:51:54 -03:00
error : ` Rate limit exceeded. Consumed (req/min): ${ rateLimiterRes . consumedPoints } , Remaining (req/min): ${ rateLimiterRes . remainingPoints } . Upgrade your plan at https://firecrawl.dev/pricing for increased rate limits or please retry after ${ secs } s, resets at ${ retryDate } ` ,
2024-12-11 19:51:08 -03:00
status : 429 ,
2024-04-20 16:38:05 -07:00
} ;
}
if (
2025-03-06 19:03:33 -03:00
token === process . env . PREVIEW_TOKEN &&
2024-08-12 13:42:09 -04:00
( mode === RateLimiterMode . Scrape ||
mode === RateLimiterMode . Preview ||
2024-08-27 20:02:39 -03:00
mode === RateLimiterMode . Map ||
2024-08-28 14:09:12 -03:00
mode === RateLimiterMode . Crawl ||
2024-08-28 14:17:59 -03:00
mode === RateLimiterMode . CrawlStatus ||
2025-01-08 17:04:57 -03:00
mode === RateLimiterMode . Extract ||
2024-08-12 13:42:09 -04:00
mode === RateLimiterMode . Search )
2024-04-20 16:38:05 -07:00
) {
2025-02-14 11:12:02 -03:00
return {
success : true ,
team_id : ` preview_ ${ iptoken } ` ,
chunk : null ,
} ;
2024-04-26 12:57:49 -07:00
// check the origin of the request and make sure its from firecrawl.dev
// const origin = req.headers.origin;
// if (origin && origin.includes("firecrawl.dev")){
// return { success: true, team_id: "preview" };
// }
// if(process.env.ENV !== "production") {
// return { success: true, team_id: "preview" };
// }
// return { success: false, error: "Unauthorized: Invalid token", status: 401 };
2024-04-20 16:38:05 -07:00
}
2024-08-12 13:42:09 -04:00
return {
success : true ,
2024-11-07 20:57:33 +01:00
team_id : teamId ? ? undefined ,
2024-12-11 19:51:08 -03:00
chunk ,
2024-08-12 13:42:09 -04:00
} ;
2024-04-20 16:38:05 -07:00
}