Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ const configSchema = z.object({
LLAMAPARSE_API_KEY: z.string().optional(),
STRIPE_SECRET_KEY: z.string().optional(),
AUTUMN_SECRET_KEY: z.string().optional(),
AUTUMN_CHECK_ENABLED: z.string().optional(),
AUTUMN_CHECK_EXPERIMENT_PERCENT: z.coerce.number().default(100),
AUTUMN_EXPERIMENT: z.string().optional(),
AUTUMN_EXPERIMENT_PERCENT: z.coerce.number().default(100),
AUTUMN_REQUEST_TRACK_EXPERIMENT: z.string().optional(),
AUTUMN_REQUEST_TRACK_EXPERIMENT_PERCENT: z.coerce.number().default(100),
RESEND_API_KEY: z.string().optional(),
PREVIEW_TOKEN: z.string().optional(),
SEARCH_PREVIEW_TOKEN: z.string().optional(),
Expand Down
40 changes: 40 additions & 0 deletions apps/api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { redlock } from "../services/redlock";
import { supabase_rr_service, supabase_service } from "../services/supabase";
import { AuthResponse, RateLimiterMode } from "../types";
import { AuthCreditUsageChunk, AuthCreditUsageChunkFromTeam } from "./v1/types";
import { isAutumnCheckEnabled } from "../services/autumn/autumn.service";

function normalizedApiIsUuid(potentialUuid: string): boolean {
// Check if the string is a valid UUID
Expand Down Expand Up @@ -387,9 +388,45 @@ export async function authenticateUser(
success: true,
chunk: null,
team_id: "bypass",
org_id: null,
})(req, res, mode);
}

/**
* Backfills org_id for stale cached auth chunks so Autumn check gating can run.
*/
async function ensureChunkOrgId(
apiKey: string,
chunk: AuthCreditUsageChunk | null,
): Promise<AuthCreditUsageChunk | null> {
if (
!chunk ||
chunk.org_id ||
config.USE_DB_AUTHENTICATION !== true ||
!isAutumnCheckEnabled()
) {
return chunk;
}

const { data, error } = await supabase_rr_service
.from("teams")
.select("org_id")
.eq("id", chunk.team_id)
.single();

if (error || !data?.org_id) {
logger.warn("Failed to backfill org_id for auth chunk", {
teamId: chunk.team_id,
error,
});
return chunk;
}

chunk.org_id = data.org_id;
await setCachedACUC(apiKey, !!chunk.is_extract, chunk);
return chunk;
}

async function supaAuthenticateUser(
req,
res,
Expand Down Expand Up @@ -449,6 +486,7 @@ async function supaAuthenticateUser(
}

chunk = await getACUC(normalizedApi, false, true, RateLimiterMode.Scrape);
chunk = await ensureChunkOrgId(normalizedApi, chunk);

if (chunk === null) {
return {
Expand Down Expand Up @@ -513,6 +551,7 @@ async function supaAuthenticateUser(
return {
success: true,
team_id: `preview_${iptoken}`,
org_id: null,
chunk: null,
};
// check the origin of the request and make sure its from firecrawl.dev
Expand Down Expand Up @@ -551,6 +590,7 @@ async function supaAuthenticateUser(
return {
success: true,
team_id: teamId ?? undefined,
org_id: chunk?.org_id ?? null,
chunk,
};
}
4 changes: 2 additions & 2 deletions apps/api/src/controllers/v1/crawl-status-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ export async function crawlStatusWSController(
});
}

const { team_id } = auth;
const { team_id, org_id } = auth;

req.auth = { team_id };
req.auth = { team_id, org_id };

await crawlStatusWS(ws, req);
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/controllers/v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@ export type CrawlErrorsResponse =

type AuthObject = {
team_id: string;
org_id?: string | null;
};

type Account = {
Expand All @@ -1229,6 +1230,7 @@ export type AuthCreditUsageChunk = {
api_key: string;
api_key_id: number;
team_id: string;
org_id?: string | null;
sub_id: string | null;
sub_current_period_start: string | null;
sub_current_period_end: string | null;
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/controllers/v2/crawl-status-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ export async function crawlStatusWSController(
});
}

const { team_id } = auth;
const { team_id, org_id } = auth;

req.auth = { team_id };
req.auth = { team_id, org_id };

await crawlStatusWS(ws, req);
} catch (err) {
Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/controllers/v2/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ export async function searchController(
zeroDataRetention = isZDROrAnon ?? false;
applyZdrScope(isZDROrAnon ?? false);

// Verify the team has searchZDR enabled before allowing enterprise ZDR/anon
if (isZDROrAnon) {
const searchMode = getSearchZDR(req.acuc?.flags);
if (searchMode !== "allowed" && searchMode !== "forced") {
return res.status(403).json({
success: false,
error:
"Zero Data Retention (ZDR) search is not enabled for your team. Contact support@firecrawl.com to enable this feature.",
});
}
}

if (!agentRequestId) {
await logRequest({
id: jobId,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/controllers/v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,7 @@ export type CrawlErrorsResponse =

type AuthObject = {
team_id: string;
org_id?: string | null;
};

type Account = {
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/controllers/v2/x402-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,20 @@ export async function x402SearchController(
origin: req.body.origin,
});

// Verify the team has searchZDR enabled before allowing enterprise ZDR/anon
const isZDR = req.body.enterprise?.includes("zdr");
const isAnon = req.body.enterprise?.includes("anon");
if (isZDR || isAnon) {
const searchMode = getSearchZDR(req.acuc?.flags);
if (searchMode !== "allowed" && searchMode !== "forced") {
return res.status(403).json({
success: false,
error:
"Zero Data Retention (ZDR) search is not enabled for your team. Contact support@firecrawl.com to enable this feature.",
});
}
}

await logRequest({
id: jobId,
kind: "search",
Expand Down
51 changes: 43 additions & 8 deletions apps/api/src/routes/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import { validate as isUuid } from "uuid";

import { config } from "../config";
import { supabase_service } from "../services/supabase";
import {
autumnService,
isAutumnCheckEnabled,
} from "../services/autumn/autumn.service";

export function checkCreditsMiddleware(
_minimum?: number,
): (req: RequestWithAuth, res: Response, next: NextFunction) => void {
Expand Down Expand Up @@ -114,12 +119,42 @@ export function checkCreditsMiddleware(
}
}

const { success, remainingCredits, chunk } = await checkTeamCredits(
req.acuc ?? null,
req.auth.team_id,
minimum ?? 1,
);
//todo(autumn) add .check call here
const requestedCredits = minimum ?? 1;
const useAutumnCheck =
!!req.auth.org_id &&
isAutumnCheckEnabled(req.auth.org_id) &&
!req.acuc?.is_extract;

const autumnProperties = {
source: "checkCreditsMiddleware",
path: req.path,
};
const [legacyCheck, autumnAllowed] = await Promise.all([
checkTeamCredits(req.acuc ?? null, req.auth.team_id, requestedCredits),
useAutumnCheck
? autumnService.checkCredits({
teamId: req.auth.team_id,
value: requestedCredits,
properties: autumnProperties,
})
: null,
]);
let { success, remainingCredits, chunk } = legacyCheck;

if (autumnAllowed !== null) {
if (autumnAllowed !== legacyCheck.success) {
logger.warn("Autumn check result diverged from legacy credit gate", {
teamId: req.auth.team_id,
path: req.path,
requestedCredits,
autumnAllowed,
legacyAllowed: legacyCheck.success,
});
}
success = autumnAllowed;
remainingCredits = legacyCheck.remainingCredits;
}

if (chunk) {
req.acuc = chunk;
}
Expand Down Expand Up @@ -204,9 +239,9 @@ export function authMiddleware(
}
}

const { team_id, chunk } = auth;
const { team_id, org_id, chunk } = auth;

req.auth = { team_id };
req.auth = { team_id, org_id };
req.acuc = chunk ?? undefined;
if (chunk) {
req.account = {
Expand Down
Loading
Loading