Skip to content

Commit 146828c

Browse files
authored
Merge pull request firecrawl#3688 from firecrawl/nick/search-zdr-forced
fix(api): allow search for teams with searchZDR forced
2 parents d2672e7 + c84dd32 commit 146828c

9 files changed

Lines changed: 122 additions & 71 deletions

File tree

apps/api/src/__tests__/snips/v2/zdr.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
scrapeStatusRaw,
77
zdrcleaner,
88
idmux,
9+
searchRaw,
910
} from "./lib";
1011
import { describeIf, TEST_PRODUCTION } from "../lib";
1112
import {
@@ -148,4 +149,20 @@ describeIf(TEST_PRODUCTION)("Zero Data Retention", () => {
148149
600000 + 20000,
149150
);
150151
});
152+
153+
it("should allow search when searchZDR is forced", async () => {
154+
const identity = await idmux({
155+
name: "zdr/search-forced",
156+
credits: 10000,
157+
flags: { searchZDR: "forced" },
158+
});
159+
160+
const response = await searchRaw(
161+
{ query: "firecrawl", limit: 1 },
162+
identity,
163+
);
164+
165+
expect(response.statusCode).toBe(200);
166+
expect(response.body.success).toBe(true);
167+
}, 60000);
151168
});

apps/api/src/controllers/v0/search.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ export async function searchController(req: Request, res: Response) {
186186
}
187187
const { team_id, chunk } = auth;
188188

189-
if (getSearchZDR(chunk?.flags) === "forced") {
189+
const v0SearchZDRMode = getSearchZDR(chunk?.flags);
190+
if (v0SearchZDRMode === "forced-zdr" || v0SearchZDRMode === "forced-anon") {
190191
return res.status(400).json({
191192
error:
192193
"Your team has zero data retention enabled. This is not supported on the v0 API. Please update your code to use the v1 API.",

apps/api/src/controllers/v1/search.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
filterDocumentsWithContent,
2727
} from "../../search/transform";
2828
import { fromV1ScrapeOptions } from "../v2/types";
29-
import { getSearchZDR } from "../../lib/zdr-helpers";
29+
import { getSearchForcedKind } from "../../lib/zdr-helpers";
3030

3131
// Used for deep research
3232
export async function searchAndScrapeSearchResult(
@@ -86,23 +86,19 @@ export async function searchController(
8686
const controllerStartTime = new Date().getTime();
8787

8888
const jobId = uuidv7();
89+
const teamForcedKind = getSearchForcedKind(req.acuc?.flags);
90+
const zeroDataRetention = teamForcedKind !== null;
91+
const teamEnterprise = teamForcedKind ? [teamForcedKind] : undefined;
8992
let logger = _logger.child({
9093
jobId,
9194
teamId: req.auth.team_id,
9295
module: "search",
9396
method: "searchController",
94-
zeroDataRetention: getSearchZDR(req.acuc?.flags) === "forced",
97+
zeroDataRetention,
98+
teamForcedKind,
9599
searchQuery: req.body.query.slice(0, 100),
96100
});
97101

98-
if (getSearchZDR(req.acuc?.flags) === "forced") {
99-
return res.status(400).json({
100-
success: false,
101-
error:
102-
"Your team has zero data retention enabled. This is not supported on search. Please contact support@firecrawl.com to unblock this feature.",
103-
});
104-
}
105-
106102
let responseData: SearchResponse = {
107103
success: true,
108104
data: [],
@@ -130,7 +126,7 @@ export async function searchController(
130126
origin: req.body.origin ?? "api",
131127
integration: req.body.integration,
132128
target_hint: req.body.query,
133-
zeroDataRetention: false,
129+
zeroDataRetention,
134130
api_key_id: req.acuc?.api_key_id ?? null,
135131
});
136132

@@ -159,6 +155,7 @@ export async function searchController(
159155
sources: [{ type: "web" }], // v1 only supports web
160156
scrapeOptions: shouldScrape ? scrapeOptions : undefined,
161157
timeout: req.body.timeout,
158+
enterprise: teamEnterprise,
162159
},
163160
{
164161
teamId: req.auth.team_id,
@@ -169,7 +166,7 @@ export async function searchController(
169166
jobId,
170167
apiVersion: "v1",
171168
bypassBilling: false,
172-
zeroDataRetention: false,
169+
zeroDataRetention,
173170
agentIndexOnly: (req as any).agentIndexOnly ?? false,
174171
},
175172
logger,
@@ -235,7 +232,7 @@ export async function searchController(
235232
scrapeOptions: undefined,
236233
},
237234
credits_cost: result.searchCredits,
238-
zeroDataRetention: false,
235+
zeroDataRetention,
239236
},
240237
false,
241238
);
@@ -276,7 +273,7 @@ export async function searchController(
276273
}
277274

278275
captureExceptionWithZdrCheck(error, {
279-
extra: { zeroDataRetention: false },
276+
extra: { zeroDataRetention },
280277
});
281278
logger.error("Unhandled error occurred in search", {
282279
version: "v1",

apps/api/src/controllers/v1/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1290,7 +1290,7 @@ export type TeamFlags = {
12901290
forceZDR?: boolean;
12911291
allowZDR?: boolean;
12921292
scrapeZDR?: "disabled" | "allowed" | "forced";
1293-
searchZDR?: "disabled" | "allowed" | "forced";
1293+
searchZDR?: "disabled" | "allowed" | "forced" | "forced-zdr" | "forced-anon";
12941294
zdrCost?: number;
12951295
checkRobotsOnScrape?: boolean;
12961296
crawlTtlHours?: number;

apps/api/src/controllers/v1/x402-search.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
captureExceptionWithZdrCheck,
2828
} from "../../services/sentry";
2929
import { getJobPriority } from "../../lib/job-priority";
30-
import { getSearchZDR } from "../../lib/zdr-helpers";
30+
import { getSearchForcedKind } from "../../lib/zdr-helpers";
3131

3232
interface DocumentWithCostTracking {
3333
document: Document;
@@ -52,7 +52,7 @@ async function scrapeX402SearchResult(
5252

5353
const costTracking = new CostTracking();
5454

55-
const zeroDataRetention = getSearchZDR(flags) === "forced";
55+
const zeroDataRetention = getSearchForcedKind(flags) !== null;
5656
applyZdrScope(zeroDataRetention);
5757

5858
try {
@@ -186,22 +186,17 @@ export async function x402SearchController(
186186
res: Response<SearchResponse & { request?: any }>,
187187
) {
188188
const jobId = uuidv7();
189+
const teamForcedKind = getSearchForcedKind(req.acuc?.flags);
190+
const zeroDataRetention = teamForcedKind !== null;
189191
let logger = _logger.child({
190192
jobId,
191193
teamId: req.auth.team_id,
192194
module: "x402-search",
193195
method: "x402SearchController",
194-
zeroDataRetention: getSearchZDR(req.acuc?.flags) === "forced",
196+
zeroDataRetention,
197+
teamForcedKind,
195198
});
196199

197-
if (getSearchZDR(req.acuc?.flags) === "forced") {
198-
return res.status(400).json({
199-
success: false,
200-
error:
201-
"Your team has zero data retention enabled. This is not supported on x402/search. Please contact support@firecrawl.com to unblock this feature.",
202-
});
203-
}
204-
205200
let responseData: SearchResponse = {
206201
success: true,
207202
data: [],
@@ -234,7 +229,7 @@ export async function x402SearchController(
234229
origin: req.body.origin ?? "api",
235230
integration: req.body.integration,
236231
target_hint: req.body.query,
237-
zeroDataRetention: false, // not supported for x402 search
232+
zeroDataRetention,
238233
api_key_id: req.acuc?.api_key_id ?? null,
239234
});
240235

@@ -351,7 +346,7 @@ export async function x402SearchController(
351346
team_id: req.auth.team_id,
352347
options: { ...req.body, scrapeOptions: undefined, query: undefined },
353348
credits_cost: responseData.data.length,
354-
zeroDataRetention: false, // not supported
349+
zeroDataRetention,
355350
},
356351
false,
357352
);
@@ -367,7 +362,7 @@ export async function x402SearchController(
367362
}
368363

369364
captureExceptionWithZdrCheck(error, {
370-
extra: { zeroDataRetention: false },
365+
extra: { zeroDataRetention },
371366
});
372367
logger.error("Unhandled error occurred in search [x402]", { error });
373368
return res.status(500).json({

apps/api/src/controllers/v2/search.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from "../../services/sentry";
2020
import { executeSearch } from "../../search/execute";
2121
import type { BillingMetadata } from "../../services/billing/types";
22-
import { getSearchZDR } from "../../lib/zdr-helpers";
22+
import { getSearchForcedKind, getSearchZDR } from "../../lib/zdr-helpers";
2323

2424
export async function searchController(
2525
req: RequestWithAuth<{}, SearchResponse, SearchRequest>,
@@ -30,28 +30,23 @@ export async function searchController(
3030
const controllerStartTime = new Date().getTime();
3131

3232
const jobId = uuidv7();
33+
const searchZDRMode = getSearchZDR(req.acuc?.flags);
34+
const teamForcedKind = getSearchForcedKind(req.acuc?.flags);
3335
let logger = _logger.child({
3436
jobId,
3537
teamId: req.auth.team_id,
3638
module: "api/v2",
3739
method: "searchController",
38-
zeroDataRetention: getSearchZDR(req.acuc?.flags) === "forced",
40+
zeroDataRetention: teamForcedKind !== null,
41+
teamForcedKind,
3942
});
4043

41-
if (getSearchZDR(req.acuc?.flags) === "forced") {
42-
return res.status(400).json({
43-
success: false,
44-
error:
45-
"Your team has zero data retention enabled. This is not supported on search. Please contact support@firecrawl.com to unblock this feature.",
46-
});
47-
}
48-
4944
const middlewareTime = controllerStartTime - middlewareStartTime;
5045
const isSearchPreview =
5146
config.SEARCH_PREVIEW_TOKEN !== undefined &&
5247
config.SEARCH_PREVIEW_TOKEN === req.body.__searchPreviewToken;
5348

54-
let zeroDataRetention = false;
49+
let zeroDataRetention = teamForcedKind !== null;
5550

5651
try {
5752
req.body = searchRequestSchema.parse(req.body);
@@ -84,16 +79,25 @@ export async function searchController(
8479
origin: req.body.origin,
8580
});
8681

82+
// Inject the team-forced enterprise mode so downstream billing,
83+
// upstream routing, and ZDR cleanup all see it.
84+
if (teamForcedKind) {
85+
const existing = req.body.enterprise ?? [];
86+
if (!existing.includes(teamForcedKind)) {
87+
req.body.enterprise = [...existing, teamForcedKind];
88+
}
89+
}
90+
8791
const isZDR = req.body.enterprise?.includes("zdr");
8892
const isAnon = req.body.enterprise?.includes("anon");
8993
const isZDROrAnon = isZDR || isAnon;
9094
zeroDataRetention = isZDROrAnon ?? false;
91-
applyZdrScope(isZDROrAnon ?? false);
95+
logger = logger.child({ zeroDataRetention });
96+
applyZdrScope(zeroDataRetention);
9297

9398
// Verify the team has searchZDR enabled before allowing enterprise ZDR/anon
94-
if (isZDROrAnon) {
95-
const searchMode = getSearchZDR(req.acuc?.flags);
96-
if (searchMode !== "allowed" && searchMode !== "forced") {
99+
if (isZDROrAnon && !teamForcedKind) {
100+
if (searchZDRMode !== "allowed") {
97101
return res.status(403).json({
98102
success: false,
99103
error:
@@ -111,7 +115,7 @@ export async function searchController(
111115
origin: req.body.origin ?? "api",
112116
integration: req.body.integration,
113117
target_hint: req.body.query,
114-
zeroDataRetention: isZDROrAnon ?? false,
118+
zeroDataRetention,
115119
api_key_id: req.acuc?.api_key_id ?? null,
116120
});
117121
}
@@ -142,7 +146,7 @@ export async function searchController(
142146
jobId,
143147
apiVersion: "v2",
144148
bypassBilling: !shouldBill,
145-
zeroDataRetention: isZDROrAnon,
149+
zeroDataRetention,
146150
billing,
147151
agentIndexOnly: (req as any).agentIndexOnly ?? false,
148152
},
@@ -180,7 +184,7 @@ export async function searchController(
180184
team_id: req.auth.team_id,
181185
options: req.body,
182186
credits_cost: shouldBill ? result.searchCredits : 0,
183-
zeroDataRetention: isZDROrAnon ?? false,
187+
zeroDataRetention,
184188
},
185189
false,
186190
);

apps/api/src/controllers/v2/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1513,7 +1513,7 @@ export type TeamFlags = {
15131513
forceZDR?: boolean;
15141514
allowZDR?: boolean;
15151515
scrapeZDR?: "disabled" | "allowed" | "forced";
1516-
searchZDR?: "disabled" | "allowed" | "forced";
1516+
searchZDR?: "disabled" | "allowed" | "forced" | "forced-zdr" | "forced-anon";
15171517
zdrCost?: number;
15181518
checkRobotsOnScrape?: boolean;
15191519
crawlTtlHours?: number;

0 commit comments

Comments
 (0)