n8n-nodes-crawlbrulee 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 crawlbrulee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # 🍮 crawlbrulee for n8n
2
+
3
+ [![npm](https://img.shields.io/npm/v/n8n-nodes-crawlbrulee?style=flat-square&label=npm)](https://www.npmjs.com/package/n8n-nodes-crawlbrulee)
4
+ [![license](https://img.shields.io/npm/l/n8n-nodes-crawlbrulee?style=flat-square&label=license)](./LICENSE)
5
+
6
+ **EU-native web scraping for AI agents & developers.**
7
+
8
+ put crawlbrulee in your n8n workflows. this community node package turns any url into clean markdown, html, links, images, metadata or a screenshot, maps a site's urls, and starts a workflow when a background scrape finishes. it also works as an [AI Agent](https://docs.n8n.io/advanced-ai/) tool, so an agent in n8n can scrape a page on its own.
9
+
10
+ - **everything runs in the EU.** the fetch, the render, the cache and your result never leave EU servers. the proxy exit is the one hop you choose: pick an EU exit and nothing leaves at all. gdpr-aligned, with a data processing agreement.
11
+ - **output made for models.** markdown with the page chrome stripped and the links kept, ready for the prompt. full-page screenshots can come back as tiles sized for an image model.
12
+ - **the hard parts, handled.** headless Chrome when a page needs it, rotating proxies with country selection, automatic retries, ad and cookie-banner removal, caching, background jobs and signed webhooks.
13
+ - **start free.** 750 credits, no credit card.
14
+
15
+ **get a free api key** → [dashboard.crawlbrulee.com](https://dashboard.crawlbrulee.com)
16
+
17
+ the package ships the **crawlbrulee** node (7 operations), the **crawlbrulee Trigger** node, and the **crawlbrulee API** credential. this readme covers them. for how the api behaves — endpoints, parameters, limits and errors — see the [api docs](https://crawlbrulee.com/docs).
18
+
19
+ ---
20
+
21
+ ## install
22
+
23
+ **n8n Cloud:** once the node is verified, open the node panel, search for "crawlbrulee", and pick the node. n8n installs it for you.
24
+
25
+ **self-hosted:** go to **Settings → Community Nodes → Install**, enter `n8n-nodes-crawlbrulee`, and confirm.
26
+
27
+ on self-hosted n8n, an AI Agent can only use a community node as a tool when the env var `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE` is `true`.
28
+
29
+ ## credentials
30
+
31
+ create an api key in the [dashboard](https://dashboard.crawlbrulee.com), then add a **crawlbrulee API** credential in n8n:
32
+
33
+ | field | what to put in it |
34
+ |---|---|
35
+ | API Key | your key, starting with `cwbl_`. required. |
36
+ | Base URL | leave as is. |
37
+ | Webhook Signing Secret | optional. starts with `whsec_`, from the dashboard under **Account → Webhooks**. |
38
+
39
+ the **crawlbrulee Trigger** checks deliveries against that secret; leave it empty and it takes them as they come. saving the credential makes a whoami call, so a bad key fails right there, at no credit cost.
40
+
41
+ ## the crawlbrulee node
42
+
43
+ pick a resource, then an operation:
44
+
45
+ | resource | operation | what it does |
46
+ |---|---|---|
47
+ | Scrape | Scrape URL | fetch one url, get back the content you asked for |
48
+ | Scrape | Scrape URL (Async) | start a background job, get back its job id |
49
+ | Scrape | Get Scrape Status | pending, running, done or failed |
50
+ | Scrape | Get Scrape Result | the content of a finished job |
51
+ | Map | Map Website | a site's urls, from its sitemap and its homepage links |
52
+ | Account | Get Credit Usage | credits used and left this billing cycle, plus your concurrency limit |
53
+ | Account | Get Account Info | the organization and token behind the api key |
54
+
55
+ ### scrape fields
56
+
57
+ every field has a hint in n8n. three things the hints don't tell you:
58
+
59
+ - **Extract** gives you exactly what you tick: the node sends every output you left unticked as an explicit "no", so the api's own defaults never add one.
60
+ - a **Screenshot** renders the page in a real browser and is billed at the screenshot rate, the priciest one — leave it on None when you don't need the image.
61
+ - **Scrape URL (Async)** takes the same fields as **Scrape URL**, minus **Download Screenshot** (a submit gives you a job id, not an image), plus **Webhook URL** and **Webhook Metadata**.
62
+
63
+ ### screenshots as files
64
+
65
+ turn on **Download Screenshot** — it's on **Scrape URL** and on **Get Scrape Result** — and the node attaches the image as binary data under `screenshot`, plus one key per tile (`screenshot_slice_0`, `screenshot_slice_1`, …) when you set Slice Height. the urls stay in the json too. a download can fail, since a signed url can expire; the item still comes through with its page content and the reason lands on the json as `screenshot_download_error`.
66
+
67
+ ### what comes back
68
+
69
+ one item in, one item out: the api's json body becomes the item's `json`, unchanged. scrape, map and finished-job responses carry `response_meta.usage`, so the credits a call cost are in front of you. **Map Website** adds `pagination` and `truncation` there, and its **Limit** starts at 50 in n8n, not the api's 5000 — n8n's rule for a field named Limit sets that, so raising it is usually the first thing you do. the [api docs](https://crawlbrulee.com/docs) have the full response shape.
70
+
71
+ set the node's "On Error" setting to continue and a failed item comes back as `{ "error": "…" }` instead of stopping the run.
72
+
73
+ ## the crawlbrulee Trigger
74
+
75
+ use it to react to a background scrape instead of polling for it:
76
+
77
+ 1. add a **crawlbrulee Trigger**, give it the same credential, and copy the webhook url it shows.
78
+ 2. paste that url into the **Webhook URL** field of a **Scrape URL (Async)** operation.
79
+ 3. when the job finishes, we post one `scrape.complete` event there and the trigger fires.
80
+
81
+ the item carries the event id, the timestamp and the job data. turn on **Fetch Result** and, for a job that succeeded, the trigger fetches the page content into `result`; if that fetch fails, the reason lands in `result_error` and the item still comes through.
82
+
83
+ with the secret set, the trigger checks the HMAC-SHA256 signature on every delivery. it drops one that fails the check, one that isn't a `scrape.complete` event, and one whose event id is among the last 500 it saw: nothing runs, and we still answer 200 so it is not retried. that id list lives in the node's workflow data, so a fresh copy of the workflow starts empty.
84
+
85
+ ## errors
86
+
87
+ the node turns api errors into messages you can act on: a rate limit says how long to wait, out of credits and concurrency limit are each named, an anti-bot block says so with no retry hint, and a scrape error points you at the Advanced proxy tier and at Require JS. the raw response stays on the error for the n8n error view, and every error name is in the [error reference](https://crawlbrulee.com/docs/errors).
88
+
89
+ ## development
90
+
91
+ ```bash
92
+ pnpm install
93
+ pnpm lint # the same rule set n8n's verification scan runs
94
+ pnpm verify # lint, format, typecheck, tests, build
95
+ pnpm dev # local n8n with this node linked, rebuilt on every change
96
+ ```
97
+
98
+ `pnpm dev` links the package into its own n8n under `~/.n8n-node-cli` and starts it on `localhost:5678` — the first run downloads n8n, so give it a while; press `o` to open it. add `--external-n8n` when you run n8n yourself, with `N8N_DEV_RELOAD=true` set there.
99
+
100
+ the package has **zero runtime dependencies**, because a verified n8n node may not ship any: `@crawlbrulee/sdk` is a types-only dev dependency, and the signature check uses the Web Crypto global.
101
+
102
+ ## part of the crawlbrulee toolkit
103
+
104
+ one api, many ways to call it:
105
+
106
+ - **[js/ts sdk](https://github.com/crawlbrulee/crawlbrulee-js)** — `@crawlbrulee/sdk`
107
+ - **[python sdk](https://github.com/crawlbrulee/crawlbrulee-py)** — `crawlbrulee` on pypi
108
+ - **[cli](https://github.com/crawlbrulee/crawlbrulee-cli)** — `npx crawlbrulee`
109
+ - **[mcp server](https://github.com/crawlbrulee/crawlbrulee-mcp)** — `@crawlbrulee/mcp`
110
+ - **[agent skills](https://github.com/crawlbrulee/crawlbrulee-skills)** — for skills-aware coding agents
111
+
112
+ docs: [crawlbrulee.com/docs](https://crawlbrulee.com/docs) · dashboard: [dashboard.crawlbrulee.com](https://dashboard.crawlbrulee.com)
113
+
114
+ ## license
115
+
116
+ [MIT](./LICENSE)
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CrawlbruleeApi = void 0;
4
+ class CrawlbruleeApi {
5
+ constructor() {
6
+ this.name = 'crawlbruleeApi';
7
+ this.displayName = 'Crawlbrulee API';
8
+ this.documentationUrl = 'https://crawlbrulee.com/docs/authentication';
9
+ this.icon = 'file:crawlbrulee.svg';
10
+ this.properties = [
11
+ {
12
+ displayName: 'API Key',
13
+ name: 'apiKey',
14
+ type: 'string',
15
+ typeOptions: { password: true },
16
+ required: true,
17
+ default: '',
18
+ description: 'Your crawlbrulee API key (starts with cwbl_). Create one in the dashboard at dashboard.crawlbrulee.com.',
19
+ },
20
+ {
21
+ displayName: 'Base URL',
22
+ name: 'baseUrl',
23
+ type: 'string',
24
+ default: 'https://api.crawlbrulee.com',
25
+ description: 'The API host. Leave as is unless you were given a different host.',
26
+ },
27
+ {
28
+ displayName: 'Webhook Signing Secret',
29
+ name: 'webhookSecret',
30
+ type: 'string',
31
+ typeOptions: { password: true },
32
+ default: '',
33
+ description: 'Optional. Your organization webhook signing secret (starts with whsec_) from the dashboard. When set, the Crawlbrulee Trigger verifies every delivery and drops any that fail. When empty, deliveries are accepted without verification.',
34
+ },
35
+ ];
36
+ this.authenticate = {
37
+ type: 'generic',
38
+ properties: {
39
+ headers: {
40
+ Authorization: '=Bearer {{$credentials.apiKey}}',
41
+ },
42
+ },
43
+ };
44
+ this.test = {
45
+ request: {
46
+ baseURL: '={{$credentials.baseUrl || "https://api.crawlbrulee.com"}}',
47
+ url: '/api/whoami',
48
+ },
49
+ };
50
+ }
51
+ }
52
+ exports.CrawlbruleeApi = CrawlbruleeApi;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128" viewBox="0 0 128 128"><image width="128" height="128" xlink:href="data:image/png;base64,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"/></svg>
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Crawlbrulee = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const binary_1 = require("./binary");
6
+ const map_1 = require("./builders/map");
7
+ const scrape_1 = require("./builders/scrape");
8
+ const descriptions_1 = require("./descriptions");
9
+ const request_1 = require("./transport/request");
10
+ class Crawlbrulee {
11
+ constructor() {
12
+ this.description = {
13
+ displayName: 'crawlbrulee',
14
+ name: 'crawlbrulee',
15
+ icon: 'file:crawlbrulee.svg',
16
+ group: ['transform'],
17
+ version: 1,
18
+ subtitle: '={{ $parameter["operation"] }}',
19
+ description: "Scrape any URL to Markdown, HTML, links, images, metadata or a screenshot, and map a site's URLs",
20
+ defaults: { name: 'crawlbrulee' },
21
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
22
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
23
+ usableAsTool: true,
24
+ credentials: [{ name: 'crawlbruleeApi', required: true }],
25
+ properties: descriptions_1.crawlbruleeProperties,
26
+ };
27
+ }
28
+ async execute() {
29
+ const items = this.getInputData();
30
+ const returnData = [];
31
+ const resource = this.getNodeParameter('resource', 0);
32
+ const operation = this.getNodeParameter('operation', 0);
33
+ for (let i = 0; i < items.length; i++) {
34
+ try {
35
+ const item = await runOperation.call(this, resource, operation, i);
36
+ returnData.push({ ...item, pairedItem: { item: i } });
37
+ }
38
+ catch (error) {
39
+ const failure = asNodeError(this.getNode(), error, i);
40
+ if (this.continueOnFail()) {
41
+ returnData.push({ json: { error: failure.message }, pairedItem: { item: i } });
42
+ continue;
43
+ }
44
+ throw failure;
45
+ }
46
+ }
47
+ return [returnData];
48
+ }
49
+ }
50
+ exports.Crawlbrulee = Crawlbrulee;
51
+ function asNodeError(node, error, itemIndex) {
52
+ if (error instanceof n8n_workflow_1.NodeApiError || error instanceof n8n_workflow_1.NodeOperationError)
53
+ return error;
54
+ return new n8n_workflow_1.NodeOperationError(node, error, { itemIndex });
55
+ }
56
+ function scrapeParams(i) {
57
+ return {
58
+ url: this.getNodeParameter('url', i),
59
+ extract: this.getNodeParameter('extract', i, []),
60
+ screenshotType: this.getNodeParameter('screenshotType', i, 'none'),
61
+ screenshotOptions: this.getNodeParameter('screenshotOptions', i, {}),
62
+ options: this.getNodeParameter('options', i, {}),
63
+ webhookUrl: this.getNodeParameter('webhookUrl', i, ''),
64
+ webhookMetadata: this.getNodeParameter('webhookMetadata', i, ''),
65
+ };
66
+ }
67
+ function jobIdSegment(node, raw, itemIndex) {
68
+ const jobId = String(raw !== null && raw !== void 0 ? raw : '').trim();
69
+ if (jobId === '') {
70
+ throw new n8n_workflow_1.NodeOperationError(node, 'Job ID is required', {
71
+ itemIndex,
72
+ description: 'Set the Job ID field to the id returned by Scrape URL (Async).',
73
+ });
74
+ }
75
+ return encodeURIComponent(jobId);
76
+ }
77
+ async function scrapeAndMaybeDownload(i, json) {
78
+ const download = this.getNodeParameter('downloadScreenshot', i, false);
79
+ if (!download)
80
+ return { json };
81
+ const binary = await binary_1.attachScreenshots.call(this, json);
82
+ return binary ? { json, binary } : { json };
83
+ }
84
+ async function runOperation(resource, operation, i) {
85
+ const node = this.getNode();
86
+ const key = `${resource}:${operation}`;
87
+ switch (key) {
88
+ case 'scrape:scrape': {
89
+ const body = (0, scrape_1.buildScrapeBody)(node, i, scrapeParams.call(this, i));
90
+ const json = await request_1.crawlbruleeRequest.call(this, {
91
+ method: 'POST',
92
+ path: '/api/scrape',
93
+ body,
94
+ itemIndex: i,
95
+ });
96
+ return scrapeAndMaybeDownload.call(this, i, json);
97
+ }
98
+ case 'scrape:scrapeAsync': {
99
+ const body = (0, scrape_1.buildAsyncScrapeBody)(node, i, scrapeParams.call(this, i));
100
+ return {
101
+ json: await request_1.crawlbruleeRequest.call(this, {
102
+ method: 'POST',
103
+ path: '/api/scrape/async',
104
+ body,
105
+ itemIndex: i,
106
+ }),
107
+ };
108
+ }
109
+ case 'scrape:getScrapeStatus': {
110
+ const jobId = jobIdSegment(node, this.getNodeParameter('jobId', i), i);
111
+ return {
112
+ json: await request_1.crawlbruleeRequest.call(this, {
113
+ method: 'GET',
114
+ path: `/api/scrape/status/${jobId}`,
115
+ itemIndex: i,
116
+ }),
117
+ };
118
+ }
119
+ case 'scrape:getScrapeResult': {
120
+ const jobId = jobIdSegment(node, this.getNodeParameter('jobId', i), i);
121
+ const json = await request_1.crawlbruleeRequest.call(this, {
122
+ method: 'GET',
123
+ path: `/api/scrape/result/${jobId}`,
124
+ itemIndex: i,
125
+ });
126
+ return scrapeAndMaybeDownload.call(this, i, json);
127
+ }
128
+ case 'map:map': {
129
+ const params = {
130
+ url: this.getNodeParameter('url', i),
131
+ options: this.getNodeParameter('options', i, {}),
132
+ };
133
+ const body = (0, map_1.buildMapBody)(node, i, params);
134
+ return {
135
+ json: await request_1.crawlbruleeRequest.call(this, {
136
+ method: 'POST',
137
+ path: '/api/map',
138
+ body,
139
+ itemIndex: i,
140
+ }),
141
+ };
142
+ }
143
+ case 'account:getUsage':
144
+ return {
145
+ json: await request_1.crawlbruleeRequest.call(this, {
146
+ method: 'GET',
147
+ path: '/api/usage',
148
+ itemIndex: i,
149
+ }),
150
+ };
151
+ case 'account:whoami':
152
+ return {
153
+ json: await request_1.crawlbruleeRequest.call(this, {
154
+ method: 'GET',
155
+ path: '/api/whoami',
156
+ itemIndex: i,
157
+ }),
158
+ };
159
+ default:
160
+ throw new n8n_workflow_1.NodeOperationError(node, `The operation "${operation}" is not supported for resource "${resource}"`, { itemIndex: i });
161
+ }
162
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "node": "n8n-nodes-crawlbrulee.crawlbrulee",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["Development", "Data & Storage"],
6
+ "resources": {
7
+ "credentialDocumentation": [{ "url": "https://crawlbrulee.com/docs/authentication" }],
8
+ "primaryDocumentation": [{ "url": "https://github.com/crawlbrulee/crawlbrulee-n8n#readme" }]
9
+ }
10
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachScreenshots = attachScreenshots;
4
+ async function download(url, fileName, mime) {
5
+ const data = (await this.helpers.httpRequest({
6
+ url,
7
+ method: 'GET',
8
+ encoding: 'arraybuffer',
9
+ }));
10
+ return this.helpers.prepareBinaryData(Buffer.from(data), fileName, mime);
11
+ }
12
+ async function attachScreenshots(json) {
13
+ var _a;
14
+ const shot = json.screenshot;
15
+ if (!(shot === null || shot === void 0 ? void 0 : shot.url))
16
+ return undefined;
17
+ const binary = {};
18
+ const failures = [];
19
+ const attach = async (key, url, fileName, mime) => {
20
+ try {
21
+ binary[key] = await download.call(this, url, fileName, mime);
22
+ }
23
+ catch (error) {
24
+ failures.push(`${key}: ${error instanceof Error ? error.message : String(error)}`);
25
+ }
26
+ };
27
+ await attach('screenshot', shot.url, shot.properties.file_name, shot.properties.mime);
28
+ for (const slice of (_a = shot.slices) !== null && _a !== void 0 ? _a : []) {
29
+ await attach(`screenshot_slice_${slice.row_nr}`, slice.url, slice.properties.file_name, slice.properties.mime);
30
+ }
31
+ if (failures.length > 0)
32
+ json.screenshot_download_error = failures.join('; ');
33
+ return Object.keys(binary).length > 0 ? binary : undefined;
34
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMapBody = buildMapBody;
4
+ const scrape_1 = require("./scrape");
5
+ function buildMapBody(_node, _itemIndex, p) {
6
+ var _a, _b;
7
+ const body = { url: p.url };
8
+ const o = (_a = p.options) !== null && _a !== void 0 ? _a : {};
9
+ if (o.sitemapOnly === true)
10
+ body.sitemap_only = true;
11
+ const types = {};
12
+ if (typeof o.internal === 'boolean')
13
+ types.internal = o.internal;
14
+ if (typeof o.internalSubdomains === 'boolean')
15
+ types.internal_subdomains = o.internalSubdomains;
16
+ if (typeof o.external === 'boolean')
17
+ types.external = o.external;
18
+ if (Object.keys(types).length > 0)
19
+ body.types = types;
20
+ if ((0, scrape_1.isSet)(o.maxUrls))
21
+ body.max_urls = Number(o.maxUrls);
22
+ if ((0, scrape_1.isSet)(o.page))
23
+ body.page = Number(o.page);
24
+ if ((0, scrape_1.isSet)(o.limit))
25
+ body.limit = Number(o.limit);
26
+ if (o.proxy)
27
+ body.proxy = o.proxy;
28
+ const maxAge = (0, scrape_1.parseCacheMaxAge)(o.cacheMaxAge);
29
+ if (maxAge !== undefined)
30
+ body.cache = { max_age: maxAge };
31
+ const country = String((_b = o.country) !== null && _b !== void 0 ? _b : '').trim();
32
+ if (country !== '')
33
+ body.location = { country };
34
+ return body;
35
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXTRACT_KEYS = void 0;
4
+ exports.parseCacheMaxAge = parseCacheMaxAge;
5
+ exports.isSet = isSet;
6
+ exports.buildScrapeBody = buildScrapeBody;
7
+ exports.buildAsyncScrapeBody = buildAsyncScrapeBody;
8
+ const n8n_workflow_1 = require("n8n-workflow");
9
+ exports.EXTRACT_KEYS = [
10
+ 'markdown',
11
+ 'cleaned_html',
12
+ 'raw_html',
13
+ 'links',
14
+ 'images',
15
+ 'metadata',
16
+ ];
17
+ function parseCacheMaxAge(value) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ const s = value.trim();
21
+ if (s === '')
22
+ return undefined;
23
+ return /^\d+$/.test(s) ? Number(s) : s;
24
+ }
25
+ function isSet(v) {
26
+ return v !== undefined && v !== null && v !== '';
27
+ }
28
+ function buildScreenshot(node, itemIndex, p) {
29
+ var _a, _b, _c;
30
+ if (p.screenshotType === 'none')
31
+ return undefined;
32
+ const o = (_a = p.screenshotOptions) !== null && _a !== void 0 ? _a : {};
33
+ const shot = { type: p.screenshotType };
34
+ const hasWidth = isSet(o.width);
35
+ const hasHeight = isSet(o.height);
36
+ if (hasWidth !== hasHeight) {
37
+ throw new n8n_workflow_1.NodeOperationError(node, 'Screenshot Width and Height must be set together', {
38
+ itemIndex,
39
+ });
40
+ }
41
+ if (hasWidth && hasHeight) {
42
+ shot.viewport = { width: Number(o.width), height: Number(o.height) };
43
+ if (isSet(o.deviceScaleFactor))
44
+ shot.viewport.device_scale_factor = Number(o.deviceScaleFactor);
45
+ }
46
+ else if (isSet(o.deviceScaleFactor)) {
47
+ throw new n8n_workflow_1.NodeOperationError(node, 'Device Scale Factor needs Screenshot Width and Height', {
48
+ itemIndex,
49
+ });
50
+ }
51
+ if (o.deviceMode === 'desktop' || o.deviceMode === 'mobile')
52
+ shot.device_mode = o.deviceMode;
53
+ const actions = (_c = (_b = o.actionsBefore) === null || _b === void 0 ? void 0 : _b.action) !== null && _c !== void 0 ? _c : [];
54
+ if (actions.length > 5) {
55
+ throw new n8n_workflow_1.NodeOperationError(node, 'Actions Before allows at most 5 actions', { itemIndex });
56
+ }
57
+ if (actions.length > 0) {
58
+ shot.actions_before = actions.map((a) => a.type === 'wait'
59
+ ? { type: 'wait', ms: Number(a.value) }
60
+ : { type: 'scroll', pixels: Number(a.value) });
61
+ }
62
+ if (isSet(o.sliceHeight)) {
63
+ const height = Number(o.sliceHeight);
64
+ if (height < 500)
65
+ throw new n8n_workflow_1.NodeOperationError(node, 'Slice Height must be at least 500 pixels', { itemIndex });
66
+ shot.actions_after = [{ type: 'slice', height }];
67
+ }
68
+ return shot;
69
+ }
70
+ function buildScrapeBody(node, itemIndex, p) {
71
+ var _a, _b, _c, _d, _e;
72
+ const chosen = new Set((_a = p.extract) !== null && _a !== void 0 ? _a : []);
73
+ if (chosen.size === 0 && p.screenshotType === 'none') {
74
+ throw new n8n_workflow_1.NodeOperationError(node, 'Pick at least one Extract output or a Screenshot', {
75
+ itemIndex,
76
+ description: 'The request would ask for nothing.',
77
+ });
78
+ }
79
+ const extract = {};
80
+ for (const key of exports.EXTRACT_KEYS)
81
+ extract[key] = chosen.has(key);
82
+ const screenshot = buildScreenshot(node, itemIndex, p);
83
+ if (screenshot)
84
+ extract.screenshot = screenshot;
85
+ const body = { url: p.url, extract };
86
+ const o = (_b = p.options) !== null && _b !== void 0 ? _b : {};
87
+ if (o.proxy)
88
+ body.proxy = o.proxy;
89
+ if (o.requireJs === true)
90
+ body.require_js = true;
91
+ const cleanup = {};
92
+ if (typeof o.removeAdsAndPopups === 'boolean')
93
+ cleanup.ads_and_popups = o.removeAdsAndPopups;
94
+ const selectors = ((_c = o.excludeSelectors) !== null && _c !== void 0 ? _c : []).map((s) => s.trim()).filter((s) => s.length > 0);
95
+ if (selectors.length > 0)
96
+ cleanup.exclude_selectors = selectors;
97
+ if (Object.keys(cleanup).length > 0)
98
+ body.cleanup = cleanup;
99
+ const maxAge = parseCacheMaxAge(o.cacheMaxAge);
100
+ if (maxAge !== undefined)
101
+ body.cache = { max_age: maxAge };
102
+ const location = {};
103
+ const locale = String((_d = o.locale) !== null && _d !== void 0 ? _d : '').trim();
104
+ const country = String((_e = o.country) !== null && _e !== void 0 ? _e : '').trim();
105
+ if (locale !== '')
106
+ location.locale = locale;
107
+ if (country !== '')
108
+ location.country = country;
109
+ if (Object.keys(location).length > 0)
110
+ body.location = location;
111
+ return body;
112
+ }
113
+ function parseMetadata(node, itemIndex, raw) {
114
+ if (raw === undefined || raw === null || raw === '')
115
+ return undefined;
116
+ if (typeof raw === 'object')
117
+ return raw;
118
+ let parsed;
119
+ let ok = true;
120
+ try {
121
+ parsed = JSON.parse(raw);
122
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
123
+ ok = false;
124
+ }
125
+ catch {
126
+ ok = false;
127
+ }
128
+ if (!ok)
129
+ throw new n8n_workflow_1.NodeOperationError(node, 'Webhook Metadata must be a JSON object', { itemIndex });
130
+ return parsed;
131
+ }
132
+ function buildAsyncScrapeBody(node, itemIndex, p) {
133
+ var _a;
134
+ const body = buildScrapeBody(node, itemIndex, p);
135
+ const url = ((_a = p.webhookUrl) !== null && _a !== void 0 ? _a : '').trim();
136
+ const metadata = parseMetadata(node, itemIndex, p.webhookMetadata);
137
+ if (!url) {
138
+ if (metadata)
139
+ throw new n8n_workflow_1.NodeOperationError(node, 'Webhook Metadata needs a Webhook URL', { itemIndex });
140
+ return body;
141
+ }
142
+ body.webhook = metadata ? { url, metadata } : { url };
143
+ return body;
144
+ }