Skip to content

Commit 6d456ae

Browse files
authored
feat(js-sdk,python-sdk): add threatProtection request option (firecrawl#3966)
1 parent 93c2bf7 commit 6d456ae

24 files changed

Lines changed: 611 additions & 17 deletions

apps/js-sdk/firecrawl/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mendable/firecrawl-js",
3-
"version": "4.29.2",
3+
"version": "4.30.0",
44
"description": "JavaScript SDK for Firecrawl API",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, test, expect, jest } from "@jest/globals";
2+
import { scrape } from "../../../v2/methods/scrape";
3+
import { startBatchScrape } from "../../../v2/methods/batch";
4+
import { startCrawl } from "../../../v2/methods/crawl";
5+
import { search } from "../../../v2/methods/search";
6+
import { map } from "../../../v2/methods/map";
7+
import { startExtract } from "../../../v2/methods/extract";
8+
import { startAgent } from "../../../v2/methods/agent";
9+
import type { ThreatProtectionOptions } from "../../../v2/types";
10+
11+
const threatProtection: ThreatProtectionOptions = {
12+
mode: "normal",
13+
riskScoreThreshold: 80,
14+
blacklist: ["*.blocked.example.com"],
15+
whitelist: ["allowed.example.com"],
16+
blockedTlds: ["zip"],
17+
failurePolicy: "open",
18+
};
19+
20+
function makeHttp(data: Record<string, unknown>) {
21+
const post = jest.fn(async () => ({ status: 200, data }));
22+
return {
23+
post,
24+
prepareHeaders: jest.fn(() => undefined),
25+
} as any;
26+
}
27+
28+
describe("v2 threatProtection request serialization", () => {
29+
test("scrape sends threatProtection at top level", async () => {
30+
const http = makeHttp({ success: true, data: {} });
31+
await scrape(http, "https://example.com", { threatProtection });
32+
expect(http.post).toHaveBeenCalledWith(
33+
"/v2/scrape",
34+
expect.objectContaining({ url: "https://example.com", threatProtection }),
35+
{},
36+
);
37+
});
38+
39+
test("batch scrape sends threatProtection at top level", async () => {
40+
const http = makeHttp({ success: true, id: "job", url: "u" });
41+
await startBatchScrape(http, ["https://example.com"], {
42+
options: { threatProtection },
43+
});
44+
expect(http.post).toHaveBeenCalledWith(
45+
"/v2/batch/scrape",
46+
expect.objectContaining({
47+
urls: ["https://example.com"],
48+
threatProtection,
49+
}),
50+
expect.anything(),
51+
);
52+
});
53+
54+
test("crawl sends threatProtection under scrapeOptions", async () => {
55+
const http = makeHttp({ success: true, id: "job", url: "u" });
56+
await startCrawl(http, {
57+
url: "https://example.com",
58+
scrapeOptions: { threatProtection },
59+
});
60+
expect(http.post).toHaveBeenCalledWith(
61+
"/v2/crawl",
62+
expect.objectContaining({
63+
scrapeOptions: expect.objectContaining({ threatProtection }),
64+
}),
65+
);
66+
});
67+
68+
test("search sends threatProtection at top level and under scrapeOptions", async () => {
69+
const http = makeHttp({ success: true, data: {} });
70+
await search(http, {
71+
query: "firecrawl",
72+
threatProtection,
73+
scrapeOptions: { threatProtection },
74+
});
75+
expect(http.post).toHaveBeenCalledWith(
76+
"/v2/search",
77+
expect.objectContaining({
78+
threatProtection,
79+
scrapeOptions: expect.objectContaining({ threatProtection }),
80+
}),
81+
{},
82+
);
83+
});
84+
85+
test("map sends threatProtection at top level", async () => {
86+
const http = makeHttp({ success: true, links: [] });
87+
await map(http, "https://example.com", { threatProtection });
88+
expect(http.post).toHaveBeenCalledWith(
89+
"/v2/map",
90+
expect.objectContaining({ threatProtection }),
91+
{},
92+
);
93+
});
94+
95+
test("extract sends threatProtection at top level", async () => {
96+
const http = makeHttp({ success: true, id: "job" });
97+
await startExtract(http, {
98+
urls: ["https://example.com"],
99+
prompt: "extract",
100+
threatProtection,
101+
});
102+
expect(http.post).toHaveBeenCalledWith(
103+
"/v2/extract",
104+
expect.objectContaining({ threatProtection }),
105+
);
106+
});
107+
108+
test("agent sends threatProtection at top level", async () => {
109+
const http = makeHttp({ success: true, id: "job", status: "processing" });
110+
await startAgent(http, { prompt: "find pricing", threatProtection });
111+
expect(http.post).toHaveBeenCalledWith(
112+
"/v2/agent",
113+
expect.objectContaining({ threatProtection }),
114+
);
115+
});
116+
117+
test("threatProtection is omitted when not provided", async () => {
118+
const http = makeHttp({ success: true, data: {} });
119+
await scrape(http, "https://example.com", { onlyMainContent: true });
120+
const body = http.post.mock.calls[0][1] as Record<string, unknown>;
121+
expect(body).not.toHaveProperty("threatProtection");
122+
});
123+
});

apps/js-sdk/firecrawl/src/v2/methods/agent.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type AgentResponse, type AgentStatusResponse, type AgentWebhookConfig } from "../types";
1+
import { type AgentResponse, type AgentStatusResponse, type AgentWebhookConfig, type ThreatProtectionOptions } from "../types";
22
import { HttpClient } from "../utils/httpClient";
33
import { normalizeAxiosError, throwForBadResponse } from "../utils/errorHandler";
44
import { isZodSchema, zodSchemaToJsonSchema } from "../../utils/zodSchemaToJson";
@@ -14,6 +14,7 @@ function prepareAgentPayload(args: {
1414
strictConstrainToURLs?: boolean;
1515
model?: "spark-1-pro" | "spark-1-mini";
1616
webhook?: string | AgentWebhookConfig;
17+
threatProtection?: ThreatProtectionOptions;
1718
}): Record<string, unknown> {
1819
const body: Record<string, unknown> = {};
1920
if (args.urls) body.urls = args.urls;
@@ -27,6 +28,8 @@ function prepareAgentPayload(args: {
2728
if (args.strictConstrainToURLs !== null && args.strictConstrainToURLs !== undefined) body.strictConstrainToURLs = args.strictConstrainToURLs;
2829
if (args.model !== null && args.model !== undefined) body.model = args.model;
2930
if (args.webhook != null) body.webhook = args.webhook;
31+
if (args.threatProtection != null)
32+
body.threatProtection = args.threatProtection;
3033
return body;
3134
}
3235

apps/js-sdk/firecrawl/src/v2/methods/extract.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ExtractResponse, type ScrapeOptions, type AgentOptions, type WebhookConfig } from "../types";
1+
import { type ExtractResponse, type ScrapeOptions, type AgentOptions, type ThreatProtectionOptions, type WebhookConfig } from "../types";
22
import { HttpClient } from "../utils/httpClient";
33
import { ensureValidScrapeOptions } from "../utils/validation";
44
import { normalizeAxiosError, throwForBadResponse } from "../utils/errorHandler";
@@ -19,6 +19,7 @@ function prepareExtractPayload(args: {
1919
origin?: string;
2020
agent?: AgentOptions;
2121
webhook?: string | WebhookConfig | null;
22+
threatProtection?: ThreatProtectionOptions;
2223
}): Record<string, unknown> {
2324
const body: Record<string, unknown> = {};
2425
if (args.urls) body.urls = args.urls;
@@ -35,6 +36,8 @@ function prepareExtractPayload(args: {
3536
if (args.origin) body.origin = args.origin;
3637
if (args.agent) body.agent = args.agent;
3738
if (args.webhook != null) body.webhook = args.webhook;
39+
if (args.threatProtection != null)
40+
body.threatProtection = args.threatProtection;
3841
if (args.scrapeOptions) {
3942
ensureValidScrapeOptions(args.scrapeOptions);
4043
body.scrapeOptions = args.scrapeOptions;

apps/js-sdk/firecrawl/src/v2/methods/map.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function prepareMapPayload(
2424
payload.integration = options.integration.trim();
2525
if (options.origin) payload.origin = options.origin;
2626
if (options.location != null) payload.location = options.location;
27+
if (options.threatProtection != null)
28+
payload.threatProtection = options.threatProtection;
2729
}
2830
return payload;
2931
}

apps/js-sdk/firecrawl/src/v2/methods/search.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function prepareSearchPayload(req: SearchRequest): Record<string, unknown> {
4141
payload.integration = req.integration.trim();
4242
if (req.origin) payload.origin = req.origin;
4343
if (req.enterprise) payload.enterprise = req.enterprise;
44+
if (req.threatProtection != null)
45+
payload.threatProtection = req.threatProtection;
4446
if (req.scrapeOptions) {
4547
ensureValidScrapeOptions(req.scrapeOptions as ScrapeOptions);
4648
payload.scrapeOptions = req.scrapeOptions;

apps/js-sdk/firecrawl/src/v2/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface ScrapeOptions {
208208
storeInCache?: boolean;
209209
lockdown?: boolean;
210210
redactPII?: boolean | RedactPIIOptions;
211+
threatProtection?: ThreatProtectionOptions;
211212
profile?: {
212213
name: string;
213214
saveChanges?: boolean;
@@ -241,6 +242,27 @@ export interface RedactPIIOptions {
241242
replaceStyle?: "tag" | "mask" | "remove";
242243
}
243244

245+
/**
246+
* Enterprise: per-request field-level override of your team's threat
247+
* protection policy. Requires threat protection to be enabled for your team
248+
* and request overrides to be allowed in the team configuration. Only the
249+
* fields you provide replace the team policy's values.
250+
*/
251+
export interface ThreatProtectionOptions {
252+
/** "off" disables scanning for this request; "normal" applies the policy. */
253+
mode?: "off" | "normal";
254+
/** Block verdicts at or above this risk score (integer 0-100). */
255+
riskScoreThreshold?: number;
256+
/** Exact domains or globs like "*.example.com" to always block (max 1000). */
257+
blacklist?: string[];
258+
/** Exact domains or globs to always allow; wins over everything (max 1000). */
259+
whitelist?: string[];
260+
/** Lowercase TLDs without the leading dot, e.g. "zip" (max 1000). */
261+
blockedTlds?: string[];
262+
/** Behavior when scanning is unavailable: "closed" blocks, "open" allows. */
263+
failurePolicy?: "open" | "closed";
264+
}
265+
244266
export type ParseFileData =
245267
| Blob
246268
| File
@@ -267,6 +289,7 @@ export type ParseOptions = Omit<
267289
| "storeInCache"
268290
| "lockdown"
269291
| "proxy"
292+
| "threatProtection"
270293
> & {
271294
formats?: ParseFormatOption[];
272295
proxy?: "basic" | "auto";
@@ -681,6 +704,7 @@ export interface SearchRequest {
681704
* your team.
682705
*/
683706
enterprise?: Array<"default" | "anon" | "zdr">;
707+
threatProtection?: ThreatProtectionOptions;
684708
integration?: string;
685709
origin?: string;
686710
}
@@ -769,6 +793,7 @@ export interface MapOptions {
769793
integration?: string;
770794
origin?: string;
771795
location?: LocationConfig;
796+
threatProtection?: ThreatProtectionOptions;
772797
}
773798

774799
export type FeedbackRating = "good" | "partial" | "bad";

apps/python-sdk/firecrawl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
V1ChangeTrackingOptions,
1818
)
1919

20-
__version__ = "4.31.0"
20+
__version__ = "4.32.0"
2121

2222
# Define the logger for the Firecrawl project
2323
logger: logging.Logger = logging.getLogger("firecrawl")
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from firecrawl.v2.types import (
2+
CrawlRequest,
3+
MapOptions,
4+
ScrapeOptions,
5+
SearchRequest,
6+
ThreatProtectionOptions,
7+
)
8+
from firecrawl.v2.methods.aio.agent import _prepare_agent_request
9+
from firecrawl.v2.methods.aio.batch import _prepare as _prepare_batch_request
10+
from firecrawl.v2.methods.aio.crawl import _prepare_crawl_request
11+
from firecrawl.v2.methods.aio.extract import _prepare_extract_request
12+
from firecrawl.v2.methods.aio.map import _prepare_map_request
13+
from firecrawl.v2.methods.aio.search import _prepare_search_request
14+
15+
16+
EXPECTED_WIRE_PAYLOAD = {
17+
"mode": "normal",
18+
"riskScoreThreshold": 80,
19+
"blacklist": ["*.blocked.example.com"],
20+
"whitelist": ["allowed.example.com"],
21+
"blockedTlds": ["zip"],
22+
"failurePolicy": "open",
23+
}
24+
25+
26+
def _threat_protection() -> ThreatProtectionOptions:
27+
return ThreatProtectionOptions(
28+
mode="normal",
29+
risk_score_threshold=80,
30+
blacklist=["*.blocked.example.com"],
31+
whitelist=["allowed.example.com"],
32+
blocked_tlds=["zip"],
33+
failure_policy="open",
34+
)
35+
36+
37+
class TestAioThreatProtectionRequestPreparation:
38+
"""The aio request preparers must serialize threat_protection too."""
39+
40+
def test_batch_scrape_top_level(self):
41+
data = _prepare_batch_request(
42+
["https://example.com"],
43+
options=ScrapeOptions(threat_protection=_threat_protection()),
44+
)
45+
assert data["threatProtection"] == EXPECTED_WIRE_PAYLOAD
46+
assert "threat_protection" not in data
47+
48+
def test_crawl_scrape_options(self):
49+
request = CrawlRequest(
50+
url="https://example.com",
51+
scrape_options=ScrapeOptions(threat_protection=_threat_protection()),
52+
)
53+
data = _prepare_crawl_request(request)
54+
assert data["scrapeOptions"]["threatProtection"] == EXPECTED_WIRE_PAYLOAD
55+
56+
def test_search_top_level_and_scrape_options(self):
57+
request = SearchRequest(
58+
query="firecrawl",
59+
threat_protection=_threat_protection(),
60+
scrape_options=ScrapeOptions(threat_protection=_threat_protection()),
61+
)
62+
data = _prepare_search_request(request)
63+
assert data["threatProtection"] == EXPECTED_WIRE_PAYLOAD
64+
assert data["scrapeOptions"]["threatProtection"] == EXPECTED_WIRE_PAYLOAD
65+
assert "threat_protection" not in data
66+
67+
def test_map_top_level(self):
68+
options = MapOptions(threat_protection=_threat_protection())
69+
data = _prepare_map_request("https://example.com", options)
70+
assert data["threatProtection"] == EXPECTED_WIRE_PAYLOAD
71+
assert "threat_protection" not in data
72+
73+
def test_extract_top_level(self):
74+
data = _prepare_extract_request(
75+
["https://example.com"],
76+
prompt="extract",
77+
threat_protection=_threat_protection(),
78+
)
79+
assert data["threatProtection"] == EXPECTED_WIRE_PAYLOAD
80+
81+
def test_agent_top_level(self):
82+
data = _prepare_agent_request(
83+
None,
84+
prompt="find pricing",
85+
threat_protection=_threat_protection(),
86+
)
87+
assert data["threatProtection"] == EXPECTED_WIRE_PAYLOAD

0 commit comments

Comments
 (0)