@mendable/firecrawl-js 4.42.0 → 4.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OCJTYRX5.js → chunk-YG554MNR.js} +1 -1
- package/dist/index.cjs +20 -4
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -5
- package/dist/{package-ECWDBUY3.js → package-FOKY5K5X.js} +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +3 -1
- package/src/__tests__/unit/v1/next-url-pinning.test.ts +81 -0
- package/src/__tests__/unit/v2/next-url-pinning.test.ts +104 -0
- package/src/utils/apiOrigin.ts +19 -0
- package/src/v1/index.ts +7 -3
- package/src/v2/methods/batch.ts +1 -1
- package/src/v2/methods/crawl.ts +1 -1
- package/src/v2/types.ts +2 -0
- package/src/v2/utils/httpClient.ts +2 -0
|
@@ -12,7 +12,7 @@ var require_package = __commonJS({
|
|
|
12
12
|
"package.json"(exports, module) {
|
|
13
13
|
module.exports = {
|
|
14
14
|
name: "@mendable/firecrawl-js",
|
|
15
|
-
version: "4.42.
|
|
15
|
+
version: "4.42.1",
|
|
16
16
|
description: "JavaScript SDK for the Firecrawl API: web scraping, crawling, web search, and scientific literature search over a research paper index of PubMed, bioRxiv, medRxiv and arXiv abstracts",
|
|
17
17
|
main: "dist/index.js",
|
|
18
18
|
types: "dist/index.d.ts",
|
package/dist/index.cjs
CHANGED
|
@@ -39,7 +39,7 @@ var require_package = __commonJS({
|
|
|
39
39
|
"package.json"(exports2, module2) {
|
|
40
40
|
module2.exports = {
|
|
41
41
|
name: "@mendable/firecrawl-js",
|
|
42
|
-
version: "4.42.
|
|
42
|
+
version: "4.42.1",
|
|
43
43
|
description: "JavaScript SDK for the Firecrawl API: web scraping, crawling, web search, and scientific literature search over a research paper index of PubMed, bioRxiv, medRxiv and arXiv abstracts",
|
|
44
44
|
main: "dist/index.js",
|
|
45
45
|
types: "dist/index.d.ts",
|
|
@@ -150,6 +150,21 @@ function getVersion() {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// src/utils/apiOrigin.ts
|
|
154
|
+
function pinToApiOrigin(apiUrl, url) {
|
|
155
|
+
const normalized = url.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
|
|
156
|
+
if (!/^([a-z][a-z\d+\-.]*:|[\\/]{2})/i.test(normalized)) return url;
|
|
157
|
+
const base = URL.canParse(apiUrl) ? new URL(apiUrl) : null;
|
|
158
|
+
if (!base?.host) {
|
|
159
|
+
throw new Error(`apiUrl must be an absolute URL, got ${JSON.stringify(apiUrl)}`);
|
|
160
|
+
}
|
|
161
|
+
const target = new URL(normalized, base);
|
|
162
|
+
const pinned = new URL(base.origin);
|
|
163
|
+
pinned.pathname = target.pathname;
|
|
164
|
+
pinned.search = target.search;
|
|
165
|
+
return pinned.href;
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
// src/v2/utils/httpClient.ts
|
|
154
169
|
var HttpClient = class {
|
|
155
170
|
instance;
|
|
@@ -190,6 +205,7 @@ var HttpClient = class {
|
|
|
190
205
|
config.headers = {
|
|
191
206
|
...config.headers || {}
|
|
192
207
|
};
|
|
208
|
+
if (config.url) config.url = pinToApiOrigin(this.apiUrl, config.url);
|
|
193
209
|
let lastError;
|
|
194
210
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
195
211
|
try {
|
|
@@ -3020,7 +3036,7 @@ var FirecrawlApp = class {
|
|
|
3020
3036
|
throw new FirecrawlError("No crawl ID provided", 400);
|
|
3021
3037
|
}
|
|
3022
3038
|
const headers = this.prepareHeaders();
|
|
3023
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}
|
|
3039
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}`, this.apiUrl);
|
|
3024
3040
|
if (skip !== void 0) {
|
|
3025
3041
|
targetURL.searchParams.set("skip", skip.toString());
|
|
3026
3042
|
}
|
|
@@ -3265,7 +3281,7 @@ var FirecrawlApp = class {
|
|
|
3265
3281
|
throw new FirecrawlError("No batch scrape ID provided", 400);
|
|
3266
3282
|
}
|
|
3267
3283
|
const headers = this.prepareHeaders();
|
|
3268
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}
|
|
3284
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}`, this.apiUrl);
|
|
3269
3285
|
if (skip !== void 0) {
|
|
3270
3286
|
targetURL.searchParams.set("skip", skip.toString());
|
|
3271
3287
|
}
|
|
@@ -3477,7 +3493,7 @@ var FirecrawlApp = class {
|
|
|
3477
3493
|
*/
|
|
3478
3494
|
async getRequest(url, headers) {
|
|
3479
3495
|
try {
|
|
3480
|
-
return await import_axios3.default.get(url, { headers });
|
|
3496
|
+
return await import_axios3.default.get(pinToApiOrigin(this.apiUrl, url), { headers });
|
|
3481
3497
|
} catch (error) {
|
|
3482
3498
|
if (error instanceof import_axios3.AxiosError && error.response) {
|
|
3483
3499
|
return error.response;
|
package/dist/index.d.cts
CHANGED
|
@@ -1676,6 +1676,8 @@ interface CrawlErrorsResponse$1 {
|
|
|
1676
1676
|
url: string;
|
|
1677
1677
|
code?: string;
|
|
1678
1678
|
error: string;
|
|
1679
|
+
/** Present when the page needs provider terms accepted first. */
|
|
1680
|
+
requiresAction?: RequiresAction;
|
|
1679
1681
|
}[];
|
|
1680
1682
|
robotsBlocked: string[];
|
|
1681
1683
|
}
|
|
@@ -2993,6 +2995,8 @@ interface CrawlErrorsResponse {
|
|
|
2993
2995
|
url: string;
|
|
2994
2996
|
code?: string;
|
|
2995
2997
|
error: string;
|
|
2998
|
+
/** Present when the page needs provider terms accepted first. */
|
|
2999
|
+
requiresAction?: RequiresAction;
|
|
2996
3000
|
}[];
|
|
2997
3001
|
/**
|
|
2998
3002
|
* URLs blocked by robots.txt
|
package/dist/index.d.ts
CHANGED
|
@@ -1676,6 +1676,8 @@ interface CrawlErrorsResponse$1 {
|
|
|
1676
1676
|
url: string;
|
|
1677
1677
|
code?: string;
|
|
1678
1678
|
error: string;
|
|
1679
|
+
/** Present when the page needs provider terms accepted first. */
|
|
1680
|
+
requiresAction?: RequiresAction;
|
|
1679
1681
|
}[];
|
|
1680
1682
|
robotsBlocked: string[];
|
|
1681
1683
|
}
|
|
@@ -2993,6 +2995,8 @@ interface CrawlErrorsResponse {
|
|
|
2993
2995
|
url: string;
|
|
2994
2996
|
code?: string;
|
|
2995
2997
|
error: string;
|
|
2998
|
+
/** Present when the page needs provider terms accepted first. */
|
|
2999
|
+
requiresAction?: RequiresAction;
|
|
2996
3000
|
}[];
|
|
2997
3001
|
/**
|
|
2998
3002
|
* URLs blocked by robots.txt
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
require_package
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YG554MNR.js";
|
|
4
4
|
|
|
5
5
|
// src/v2/utils/httpClient.ts
|
|
6
6
|
import axios from "axios";
|
|
@@ -18,6 +18,21 @@ function getVersion() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/apiOrigin.ts
|
|
22
|
+
function pinToApiOrigin(apiUrl, url) {
|
|
23
|
+
const normalized = url.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
|
|
24
|
+
if (!/^([a-z][a-z\d+\-.]*:|[\\/]{2})/i.test(normalized)) return url;
|
|
25
|
+
const base = URL.canParse(apiUrl) ? new URL(apiUrl) : null;
|
|
26
|
+
if (!base?.host) {
|
|
27
|
+
throw new Error(`apiUrl must be an absolute URL, got ${JSON.stringify(apiUrl)}`);
|
|
28
|
+
}
|
|
29
|
+
const target = new URL(normalized, base);
|
|
30
|
+
const pinned = new URL(base.origin);
|
|
31
|
+
pinned.pathname = target.pathname;
|
|
32
|
+
pinned.search = target.search;
|
|
33
|
+
return pinned.href;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
// src/v2/utils/httpClient.ts
|
|
22
37
|
var HttpClient = class {
|
|
23
38
|
instance;
|
|
@@ -58,6 +73,7 @@ var HttpClient = class {
|
|
|
58
73
|
config.headers = {
|
|
59
74
|
...config.headers || {}
|
|
60
75
|
};
|
|
76
|
+
if (config.url) config.url = pinToApiOrigin(this.apiUrl, config.url);
|
|
61
77
|
let lastError;
|
|
62
78
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
63
79
|
try {
|
|
@@ -2673,7 +2689,7 @@ var FirecrawlApp = class {
|
|
|
2673
2689
|
if (typeof process !== "undefined" && process.env && process.env.npm_package_version) {
|
|
2674
2690
|
return process.env.npm_package_version;
|
|
2675
2691
|
}
|
|
2676
|
-
const packageJson = await import("./package-
|
|
2692
|
+
const packageJson = await import("./package-FOKY5K5X.js");
|
|
2677
2693
|
return packageJson.default.version;
|
|
2678
2694
|
} catch (error) {
|
|
2679
2695
|
const isTest = typeof process !== "undefined" && (process.env.JEST_WORKER_ID != null || false);
|
|
@@ -2888,7 +2904,7 @@ var FirecrawlApp = class {
|
|
|
2888
2904
|
throw new FirecrawlError("No crawl ID provided", 400);
|
|
2889
2905
|
}
|
|
2890
2906
|
const headers = this.prepareHeaders();
|
|
2891
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}
|
|
2907
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}`, this.apiUrl);
|
|
2892
2908
|
if (skip !== void 0) {
|
|
2893
2909
|
targetURL.searchParams.set("skip", skip.toString());
|
|
2894
2910
|
}
|
|
@@ -3133,7 +3149,7 @@ var FirecrawlApp = class {
|
|
|
3133
3149
|
throw new FirecrawlError("No batch scrape ID provided", 400);
|
|
3134
3150
|
}
|
|
3135
3151
|
const headers = this.prepareHeaders();
|
|
3136
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}
|
|
3152
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}`, this.apiUrl);
|
|
3137
3153
|
if (skip !== void 0) {
|
|
3138
3154
|
targetURL.searchParams.set("skip", skip.toString());
|
|
3139
3155
|
}
|
|
@@ -3345,7 +3361,7 @@ var FirecrawlApp = class {
|
|
|
3345
3361
|
*/
|
|
3346
3362
|
async getRequest(url, headers) {
|
|
3347
3363
|
try {
|
|
3348
|
-
return await axios2.get(url, { headers });
|
|
3364
|
+
return await axios2.get(pinToApiOrigin(this.apiUrl, url), { headers });
|
|
3349
3365
|
} catch (error) {
|
|
3350
3366
|
if (error instanceof AxiosError && error.response) {
|
|
3351
3367
|
return error.response;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mendable/firecrawl-js",
|
|
3
|
-
"version": "4.42.
|
|
3
|
+
"version": "4.42.1",
|
|
4
4
|
"description": "JavaScript SDK for the Firecrawl API: web scraping, crawling, web search, and scientific literature search over a research paper index of PubMed, bioRxiv, medRxiv and arXiv abstracts",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -7,7 +7,9 @@ overrides:
|
|
|
7
7
|
"rollup@<4.59.0": ">=4.59.0"
|
|
8
8
|
"picomatch@<4.0.4": ">=4.0.4"
|
|
9
9
|
handlebars: ">=4.7.9"
|
|
10
|
-
|
|
10
|
+
# GHSA-q2hr-2g5m-vwhr / GHSA-qhr7-859c-m2p7 / GHSA-6j4f-fj2g-mc7p:
|
|
11
|
+
# pin the 5.x line only. Exact 5.0.12 cannot resolve to 6.x.
|
|
12
|
+
"brace-expansion@>=5.0.0 <5.0.12": "5.0.12"
|
|
11
13
|
"js-yaml@>=3.0.0 <3.15.2": "3.15.2"
|
|
12
14
|
"js-yaml@>=4.0.0 <4.3.2": "4.3.2"
|
|
13
15
|
"axios@>=1.0.0 <1.16.0": "1.16.1"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect, jest, afterEach } from "@jest/globals";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import FirecrawlApp from "../../../v1";
|
|
4
|
+
|
|
5
|
+
const API_URL = "https://api.firecrawl.dev";
|
|
6
|
+
const API_KEY = "fc-test";
|
|
7
|
+
|
|
8
|
+
const origins: Array<[string, string]> = [
|
|
9
|
+
["same origin", API_URL],
|
|
10
|
+
["cross host", "https://evil.example"],
|
|
11
|
+
["protocol-relative", "//evil.example"],
|
|
12
|
+
["different port", "https://api.firecrawl.dev:8443"],
|
|
13
|
+
["different scheme", "http://api.firecrawl.dev"],
|
|
14
|
+
["whitespace-prefixed", "\n https://evil.example"],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function mockGet(respond: (url: URL) => unknown) {
|
|
18
|
+
const sent: Array<{ url: string; authorization: unknown }> = [];
|
|
19
|
+
jest.spyOn(axios, "get").mockImplementation(async (url: string, config?: any) => {
|
|
20
|
+
sent.push({ url, authorization: config?.headers?.Authorization });
|
|
21
|
+
return { status: 200, data: respond(new URL(url)) };
|
|
22
|
+
});
|
|
23
|
+
return sent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expectPinned(sent: Array<{ url: string; authorization: unknown }>, expectedUrls: string[]) {
|
|
27
|
+
expect(sent.map(req => req.url)).toEqual(expectedUrls);
|
|
28
|
+
for (const req of sent) {
|
|
29
|
+
expect(new URL(req.url).origin).toBe(API_URL);
|
|
30
|
+
expect(req.url).not.toContain("evil.example");
|
|
31
|
+
expect(req.authorization).toBe(`Bearer ${API_KEY}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const completedPage = (next: string) => ({
|
|
36
|
+
success: true,
|
|
37
|
+
status: "completed",
|
|
38
|
+
total: 2,
|
|
39
|
+
completed: 2,
|
|
40
|
+
expiresAt: "2030-01-01T00:00:00Z",
|
|
41
|
+
data: [{ markdown: "a" }],
|
|
42
|
+
next,
|
|
43
|
+
});
|
|
44
|
+
const lastPage = () => ({ success: true, status: "completed", data: [{ markdown: "b" }] });
|
|
45
|
+
|
|
46
|
+
describe("v1 pins next URLs to the api_url origin", () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe.each(origins)("%s next", (_label, origin) => {
|
|
52
|
+
const app = new FirecrawlApp({ apiKey: API_KEY, apiUrl: API_URL });
|
|
53
|
+
|
|
54
|
+
test.each([
|
|
55
|
+
["checkCrawlStatus", "/v1/crawl/abc"],
|
|
56
|
+
["checkBatchScrapeStatus", "/v1/batch/scrape/abc"],
|
|
57
|
+
] as const)("%s getAllData follows server next", async (method, path) => {
|
|
58
|
+
const sent = mockGet(url => (url.searchParams.has("skip") ? lastPage() : completedPage(`${origin}${path}?skip=10`)));
|
|
59
|
+
const res: any = await app[method]("abc", true);
|
|
60
|
+
expect(res.data).toHaveLength(2);
|
|
61
|
+
expectPinned(sent, [`${API_URL}${path}`, `${API_URL}${path}?skip=10`]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test.each([
|
|
65
|
+
["checkCrawlStatus", "/v1/crawl/abc"],
|
|
66
|
+
["checkBatchScrapeStatus", "/v1/batch/scrape/abc"],
|
|
67
|
+
] as const)("%s caller-supplied nextURL", async (method, path) => {
|
|
68
|
+
const sent = mockGet(lastPage);
|
|
69
|
+
await app[method]("abc", false, `${origin}${path}?skip=10`);
|
|
70
|
+
expectPinned(sent, [`${API_URL}${path}?skip=10`]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("monitorJobStatus follows server next", async () => {
|
|
74
|
+
const path = "/v1/crawl/abc";
|
|
75
|
+
const sent = mockGet(url => (url.searchParams.has("skip") ? lastPage() : completedPage(`${origin}${path}?skip=10`)));
|
|
76
|
+
const res: any = await app.monitorJobStatus("abc", app.prepareHeaders(), 0);
|
|
77
|
+
expect(res.data).toHaveLength(2);
|
|
78
|
+
expectPinned(sent, [`${API_URL}${path}`, `${API_URL}${path}?skip=10`]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, test, expect } from "@jest/globals";
|
|
2
|
+
import axios, { type AxiosAdapter } from "axios";
|
|
3
|
+
import { FirecrawlClient } from "../../../v2/client";
|
|
4
|
+
import { pinToApiOrigin } from "../../../utils/apiOrigin";
|
|
5
|
+
|
|
6
|
+
const API_URL = "https://api.firecrawl.dev";
|
|
7
|
+
const API_KEY = "fc-test";
|
|
8
|
+
|
|
9
|
+
const origins: Array<[string, string]> = [
|
|
10
|
+
["same origin", API_URL],
|
|
11
|
+
["cross host", "https://evil.example"],
|
|
12
|
+
["protocol-relative", "//evil.example"],
|
|
13
|
+
["different port", "https://api.firecrawl.dev:8443"],
|
|
14
|
+
["different scheme", "http://api.firecrawl.dev"],
|
|
15
|
+
["whitespace-prefixed", "\n https://evil.example"],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function makeClient(firstPage: (next: string) => unknown, lastPage: unknown, next: string) {
|
|
19
|
+
const client = new FirecrawlClient({ apiKey: API_KEY, apiUrl: API_URL });
|
|
20
|
+
const sent: Array<{ url: string; authorization: unknown }> = [];
|
|
21
|
+
const adapter: AxiosAdapter = async config => {
|
|
22
|
+
const url = axios.getUri(config);
|
|
23
|
+
sent.push({ url, authorization: config.headers.Authorization });
|
|
24
|
+
const data = new URL(url).searchParams.has("skip") ? lastPage : firstPage(next);
|
|
25
|
+
return { data, status: 200, statusText: "OK", headers: {}, config };
|
|
26
|
+
};
|
|
27
|
+
(client as any).http.instance.defaults.adapter = adapter;
|
|
28
|
+
return { client, sent };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function expectPinned(sent: Array<{ url: string; authorization: unknown }>, path: string) {
|
|
32
|
+
expect(sent).toHaveLength(2);
|
|
33
|
+
expect(sent[1]!.url).toBe(`${API_URL}${path}?skip=10`);
|
|
34
|
+
for (const req of sent) {
|
|
35
|
+
expect(new URL(req.url).origin).toBe(API_URL);
|
|
36
|
+
expect(req.url).not.toContain("evil.example");
|
|
37
|
+
expect(req.authorization).toBe(`Bearer ${API_KEY}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("v2 pagination pins next URLs to the api_url origin", () => {
|
|
42
|
+
describe.each(origins)("%s next", (_label, origin) => {
|
|
43
|
+
test("crawl status", async () => {
|
|
44
|
+
const path = "/v2/crawl/abc";
|
|
45
|
+
const { client, sent } = makeClient(
|
|
46
|
+
next => ({ success: true, status: "completed", completed: 2, total: 2, next, data: [{ markdown: "a" }] }),
|
|
47
|
+
{ success: true, next: null, data: [{ markdown: "b" }] },
|
|
48
|
+
`${origin}${path}?skip=10`,
|
|
49
|
+
);
|
|
50
|
+
const job = await client.getCrawlStatus("abc");
|
|
51
|
+
expect(job.data).toHaveLength(2);
|
|
52
|
+
expectPinned(sent, path);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("batch scrape status", async () => {
|
|
56
|
+
const path = "/v2/batch/scrape/abc";
|
|
57
|
+
const { client, sent } = makeClient(
|
|
58
|
+
next => ({ success: true, status: "completed", completed: 2, total: 2, next, data: [{ markdown: "a" }] }),
|
|
59
|
+
{ success: true, next: null, data: [{ markdown: "b" }] },
|
|
60
|
+
`${origin}${path}?skip=10`,
|
|
61
|
+
);
|
|
62
|
+
const job = await client.getBatchScrapeStatus("abc");
|
|
63
|
+
expect(job.data).toHaveLength(2);
|
|
64
|
+
expectPinned(sent, path);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("monitor check", async () => {
|
|
68
|
+
const path = "/v2/monitor/m1/checks/c1";
|
|
69
|
+
const { client, sent } = makeClient(
|
|
70
|
+
next => ({ success: true, data: { id: "c1", pages: [{ url: "https://a.example" }], next } }),
|
|
71
|
+
{ success: true, data: { pages: [{ url: "https://b.example" }], next: null } },
|
|
72
|
+
`${origin}${path}?skip=10`,
|
|
73
|
+
);
|
|
74
|
+
const check = await client.getMonitorCheck("m1", "c1");
|
|
75
|
+
expect(check.pages).toHaveLength(2);
|
|
76
|
+
expectPinned(sent, path);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("pinToApiOrigin", () => {
|
|
82
|
+
test("rewrites absolute URLs onto the api origin, keeping path and query and dropping the fragment", () => {
|
|
83
|
+
expect(pinToApiOrigin(API_URL, "https://evil.example/v2/crawl/abc?skip=10#frag")).toBe(
|
|
84
|
+
`${API_URL}/v2/crawl/abc?skip=10`,
|
|
85
|
+
);
|
|
86
|
+
expect(pinToApiOrigin("http://localhost:3002", "https://evil.example:444/v2/crawl/abc?skip=10")).toBe(
|
|
87
|
+
"http://localhost:3002/v2/crawl/abc?skip=10",
|
|
88
|
+
);
|
|
89
|
+
expect(pinToApiOrigin(API_URL, "\\\\evil.example/v2/crawl/abc")).toBe(`${API_URL}/v2/crawl/abc`);
|
|
90
|
+
for (const url of ["\nhttps://evil.example/v2/crawl/abc", "h\tttps://evil.example/v2/crawl/abc", "\u0000 //evil.example/v2/crawl/abc", "/\\evil.example/v2/crawl/abc"]) {
|
|
91
|
+
expect(pinToApiOrigin(API_URL, url)).toBe(`${API_URL}/v2/crawl/abc`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("leaves relative URLs unchanged", () => {
|
|
96
|
+
expect(pinToApiOrigin(API_URL, "/v2/crawl/abc?skip=10")).toBe("/v2/crawl/abc?skip=10");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("throws when apiUrl is not absolute and the URL needs pinning", () => {
|
|
100
|
+
expect(() => pinToApiOrigin("api.firecrawl.dev", "https://evil.example/v2/crawl/abc")).toThrow(
|
|
101
|
+
"apiUrl must be an absolute URL",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite an absolute or protocol-relative URL onto apiUrl's scheme, host and
|
|
3
|
+
* port (keeping path and query, dropping any fragment) so credentials never
|
|
4
|
+
* leave the configured API origin. Relative URLs are returned unchanged.
|
|
5
|
+
* Throws if apiUrl is not an absolute URL.
|
|
6
|
+
*/
|
|
7
|
+
export function pinToApiOrigin(apiUrl: string, url: string): string {
|
|
8
|
+
const normalized = url.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
|
|
9
|
+
if (!/^([a-z][a-z\d+\-.]*:|[\\/]{2})/i.test(normalized)) return url;
|
|
10
|
+
const base = URL.canParse(apiUrl) ? new URL(apiUrl) : null;
|
|
11
|
+
if (!base?.host) {
|
|
12
|
+
throw new Error(`apiUrl must be an absolute URL, got ${JSON.stringify(apiUrl)}`);
|
|
13
|
+
}
|
|
14
|
+
const target = new URL(normalized, base);
|
|
15
|
+
const pinned = new URL(base.origin);
|
|
16
|
+
pinned.pathname = target.pathname;
|
|
17
|
+
pinned.search = target.search;
|
|
18
|
+
return pinned.href;
|
|
19
|
+
}
|
package/src/v1/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import axios, { type AxiosResponse, type AxiosRequestHeaders, AxiosError } from
|
|
|
2
2
|
import * as zt from "zod";
|
|
3
3
|
import { zodSchemaToJsonSchema } from "../utils/zodSchemaToJson";
|
|
4
4
|
import { TypedEventTarget } from "typescript-event-target";
|
|
5
|
+
import type { RequiresAction } from "../v2/types";
|
|
6
|
+
import { pinToApiOrigin } from "../utils/apiOrigin";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Configuration interface for FirecrawlApp.
|
|
@@ -439,6 +441,8 @@ export interface CrawlErrorsResponse {
|
|
|
439
441
|
url: string,
|
|
440
442
|
code?: string,
|
|
441
443
|
error: string,
|
|
444
|
+
/** Present when the page needs provider terms accepted first. */
|
|
445
|
+
requiresAction?: RequiresAction,
|
|
442
446
|
}[];
|
|
443
447
|
|
|
444
448
|
/**
|
|
@@ -919,7 +923,7 @@ export default class FirecrawlApp {
|
|
|
919
923
|
}
|
|
920
924
|
|
|
921
925
|
const headers: AxiosRequestHeaders = this.prepareHeaders();
|
|
922
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}
|
|
926
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/crawl/${id}`, this.apiUrl);
|
|
923
927
|
if (skip !== undefined) {
|
|
924
928
|
targetURL.searchParams.set("skip", skip.toString());
|
|
925
929
|
}
|
|
@@ -1207,7 +1211,7 @@ export default class FirecrawlApp {
|
|
|
1207
1211
|
}
|
|
1208
1212
|
|
|
1209
1213
|
const headers: AxiosRequestHeaders = this.prepareHeaders();
|
|
1210
|
-
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}
|
|
1214
|
+
const targetURL = new URL(nextURL ?? `${this.apiUrl}/v1/batch/scrape/${id}`, this.apiUrl);
|
|
1211
1215
|
if (skip !== undefined) {
|
|
1212
1216
|
targetURL.searchParams.set("skip", skip.toString());
|
|
1213
1217
|
}
|
|
@@ -1448,7 +1452,7 @@ export default class FirecrawlApp {
|
|
|
1448
1452
|
headers: AxiosRequestHeaders
|
|
1449
1453
|
): Promise<AxiosResponse> {
|
|
1450
1454
|
try {
|
|
1451
|
-
return await axios.get(url, { headers });
|
|
1455
|
+
return await axios.get(pinToApiOrigin(this.apiUrl, url), { headers });
|
|
1452
1456
|
} catch (error) {
|
|
1453
1457
|
if (error instanceof AxiosError && error.response) {
|
|
1454
1458
|
return error.response as AxiosResponse;
|
package/src/v2/methods/batch.ts
CHANGED
|
@@ -152,7 +152,7 @@ export async function getBatchScrapeErrors(
|
|
|
152
152
|
try {
|
|
153
153
|
const res = await http.get<{
|
|
154
154
|
success?: boolean;
|
|
155
|
-
data?:
|
|
155
|
+
data?: CrawlErrorsResponse;
|
|
156
156
|
}>(`/v2/batch/scrape/${jobId}/errors`);
|
|
157
157
|
if (res.status !== 200) throwForBadResponse(res, "get batch scrape errors");
|
|
158
158
|
const payload = res.data?.data ?? (res.data as any);
|
package/src/v2/methods/crawl.ts
CHANGED
|
@@ -162,7 +162,7 @@ export async function crawl(http: HttpClient, request: CrawlRequest, pollInterva
|
|
|
162
162
|
|
|
163
163
|
export async function getCrawlErrors(http: HttpClient, crawlId: string): Promise<CrawlErrorsResponse> {
|
|
164
164
|
try {
|
|
165
|
-
const res = await http.get<{ success?: boolean; data?:
|
|
165
|
+
const res = await http.get<{ success?: boolean; data?: CrawlErrorsResponse }>(`/v2/crawl/${crawlId}/errors`);
|
|
166
166
|
if (res.status !== 200) throwForBadResponse(res, "get crawl errors");
|
|
167
167
|
const payload = res.data?.data ?? (res.data as any);
|
|
168
168
|
return { errors: payload.errors || [], robotsBlocked: payload.robotsBlocked || [] };
|
package/src/v2/types.ts
CHANGED
|
@@ -1955,6 +1955,8 @@ export interface CrawlErrorsResponse {
|
|
|
1955
1955
|
url: string;
|
|
1956
1956
|
code?: string;
|
|
1957
1957
|
error: string;
|
|
1958
|
+
/** Present when the page needs provider terms accepted first. */
|
|
1959
|
+
requiresAction?: RequiresAction;
|
|
1958
1960
|
}[];
|
|
1959
1961
|
robotsBlocked: string[];
|
|
1960
1962
|
}
|
|
@@ -4,6 +4,7 @@ import axios, {
|
|
|
4
4
|
type AxiosResponse,
|
|
5
5
|
} from "axios";
|
|
6
6
|
import { getVersion } from "./getVersion";
|
|
7
|
+
import { pinToApiOrigin } from "../../utils/apiOrigin";
|
|
7
8
|
|
|
8
9
|
export interface HttpClientOptions {
|
|
9
10
|
apiKey: string;
|
|
@@ -64,6 +65,7 @@ export class HttpClient {
|
|
|
64
65
|
config.headers = {
|
|
65
66
|
...(config.headers || {}),
|
|
66
67
|
};
|
|
68
|
+
if (config.url) config.url = pinToApiOrigin(this.apiUrl, config.url);
|
|
67
69
|
|
|
68
70
|
let lastError: any;
|
|
69
71
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|