capskip 1.3.0 → 1.5.0

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.
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pass a DataDome challenge and fetch the page with the cookie.
5
+ *
6
+ * DataDome answers a request it doubts with a block page (status 403, sometimes
7
+ * 401) instead of the site. That page carries a `var dd={...}` object and loads
8
+ * the challenge in an iframe from geo.captcha-delivery.com: a slider under
9
+ * /captcha/, or a few-second device check under /interstitial/. CapSkip passes
10
+ * either and returns the `datadome` cookie.
11
+ *
12
+ * The cookie belongs to one IP address and one User-Agent, so:
13
+ *
14
+ * * solve through the proxy your requests use (or none, if they leave from
15
+ * the machine running CapSkip), and
16
+ * * send the User-Agent CapSkip reports back with every request.
17
+ *
18
+ * When you send no proxy and the CapSkip app has its own DataDome proxy list,
19
+ * it solves through one of those and the result names it as `proxy`
20
+ * (host:port): the cookie then only works through that proxy.
21
+ *
22
+ * Requests go out with Node's built-in fetch. Sending them through a proxy
23
+ * needs undici's ProxyAgent (`npm install undici`); without it, the example
24
+ * prints what to do instead of re-fetching through the proxy.
25
+ *
26
+ * Environment:
27
+ * TARGET_URL a page behind DataDome (default: https://www.example.com/)
28
+ * DATADOME_PROXY optional, e.g. http://login:password@203.0.113.10:8080
29
+ * CAPSKIP_API_KEY / CAPSKIP_HOST / CAPSKIP_PORT
30
+ */
31
+
32
+ const { CapSkip } = require('capskip');
33
+
34
+ const solver = new CapSkip({
35
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
36
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
37
+ port: Number(process.env.CAPSKIP_PORT || 8080),
38
+ });
39
+
40
+ const PAGE_URL = process.env.TARGET_URL || 'https://www.example.com/';
41
+ const PROXY = process.env.DATADOME_PROXY; // scheme://[login:password@]host:port, or unset
42
+
43
+ let ProxyAgent;
44
+ try {
45
+ ({ ProxyAgent } = require('undici'));
46
+ } catch (error) {
47
+ ProxyAgent = undefined;
48
+ }
49
+
50
+ /** The challenge iframe address, as the block page's own script builds it. */
51
+ function captchaUrlFromBlockPage(html, pageUrl) {
52
+ const iframe = html.match(/<iframe[^>]+src="(https:\/\/[^"]*captcha-delivery\.com\/[^"]+)"/);
53
+ if (iframe) {
54
+ return iframe[1]; // HTML-escaped &amp; is fine, CapSkip decodes it
55
+ }
56
+ const match = html.match(/var dd=(\{[^}]*\})/);
57
+ if (!match) {
58
+ return null;
59
+ }
60
+ const dd = JSON.parse(match[1].replace(/'/g, '"'));
61
+ const path = dd.rt === 'i' ? 'interstitial' : 'captcha';
62
+ const query = new URLSearchParams({ initialCid: dd.cid, hash: dd.hsh, cid: dd.cookie });
63
+ if (dd.t) {
64
+ query.set('t', dd.t);
65
+ }
66
+ query.set('referer', pageUrl);
67
+ query.set('s', String(dd.s));
68
+ for (const key of ['e', 'b']) {
69
+ if (dd[key]) {
70
+ query.set(key, String(dd[key]));
71
+ }
72
+ }
73
+ query.set('dm', 'cd');
74
+ return `https://${dd.host || 'geo.captcha-delivery.com'}/${path}/?${query}`;
75
+ }
76
+
77
+ /** fetch, through `proxyUrl` when one is given and undici is installed. */
78
+ function get(url, { proxyUrl, headers } = {}) {
79
+ const options = { headers, redirect: 'follow' };
80
+ if (proxyUrl) {
81
+ options.dispatcher = new ProxyAgent(proxyUrl);
82
+ }
83
+ return fetch(url, options);
84
+ }
85
+
86
+ async function main() {
87
+ if (PROXY && !ProxyAgent) {
88
+ console.log('DATADOME_PROXY is set: run `npm install undici` so the requests use it.');
89
+ return;
90
+ }
91
+
92
+ // 1. Request the page. A 403/401 with a dd object is DataDome's block page.
93
+ const response = await get(PAGE_URL, { proxyUrl: PROXY });
94
+ const html = await response.text();
95
+ const captchaUrl = captchaUrlFromBlockPage(html, PAGE_URL);
96
+ if (![401, 403].includes(response.status) || !captchaUrl) {
97
+ console.log(`No DataDome block page at ${PAGE_URL} (HTTP ${response.status}).`);
98
+ return;
99
+ }
100
+ console.log('Block page:', `${captchaUrl.slice(0, 90)}...`);
101
+
102
+ // 2. Solve it. 2Captcha's argument order: datadome(captcha_url, pageurl,
103
+ // userAgent, proxy), with userAgent and proxy as options. No userAgent:
104
+ // CapSkip uses its own browser's and reports it. With one, it must be
105
+ // Chrome for Windows with the same major version as that browser.
106
+ const options = {};
107
+ if (PROXY) {
108
+ const scheme = PROXY.includes('://') ? PROXY.split('://', 1)[0] : 'http';
109
+ options.proxy = { type: scheme.toUpperCase(), uri: PROXY.split('://').pop() };
110
+ }
111
+ const result = await solver.dataDome(captchaUrl, PAGE_URL, options);
112
+
113
+ console.log('Captcha ID:', result.captchaId);
114
+ console.log('Cookie: ', `${result.cookie.slice(0, 60)}...`);
115
+ console.log('User-Agent:', result.userAgent);
116
+ if (result.proxy) {
117
+ console.log('Proxy: ', result.proxy, '(from the CapSkip app DataDome list; the cookie only works through it)');
118
+ }
119
+
120
+ // 3. Repeat the request with the cookie, the same User-Agent and the same IP.
121
+ let proxyUrl = PROXY;
122
+ if (result.proxy && !PROXY) {
123
+ // Named without its login: add yours if that list entry has one.
124
+ if (!ProxyAgent) {
125
+ console.log(`Send the cookie through http://${result.proxy}: run \`npm install undici\` to do it here.`);
126
+ return;
127
+ }
128
+ proxyUrl = `http://${result.proxy}`;
129
+ }
130
+ // `cookie` is the datadome=... pair, ready for the Cookie header; `code`
131
+ // keeps the attributes (Domain, Max-Age...) for a cookie jar.
132
+ const again = await get(PAGE_URL, {
133
+ proxyUrl,
134
+ headers: { 'User-Agent': result.userAgent, Cookie: result.cookie },
135
+ });
136
+ console.log('With the cookie: HTTP', again.status);
137
+ }
138
+
139
+ main().catch((error) => {
140
+ console.error(`${error.name}: ${error.message}`);
141
+ process.exitCode = 1;
142
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "capskip",
3
- "version": "1.3.0",
4
- "description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
3
+ "version": "1.5.0",
4
+ "description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, AWS WAF, DataDome and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
7
7
  "exports": {
@@ -39,6 +39,10 @@
39
39
  "cloudflare-turnstile",
40
40
  "geetest",
41
41
  "geetest-solver",
42
+ "aws-waf",
43
+ "aws-waf-captcha",
44
+ "datadome",
45
+ "datadome-solver",
42
46
  "image-captcha",
43
47
  "ocr",
44
48
  "2captcha-alternative",
package/src/apiParams.js CHANGED
@@ -46,6 +46,42 @@ const FRIENDLY_CAPTCHA_SUBMIT = new Set([
46
46
  'api_server', 'useragent', 'json', 'proxy', 'proxytype',
47
47
  ]);
48
48
 
49
+ const AMAZON_WAF_SUBMIT = new Set([
50
+ 'method', 'sitekey', 'pageurl', 'iv', 'context', 'challenge_script',
51
+ 'captcha_script', 'jsapiScript', 'json', 'proxy', 'proxytype',
52
+ ]);
53
+
54
+ // The script parameters of an AWS WAF task. CapSkip POSTs the solution to the
55
+ // host these name, so it refuses any that is not a genuine AWS WAF one.
56
+ const AMAZON_WAF_SCRIPTS = ['challenge_script', 'captcha_script', 'jsapiScript'];
57
+
58
+ // The server's own test: one or more [A-Za-z0-9_-] path segments, then one of
59
+ // the three AWS WAF script files.
60
+ const AMAZON_WAF_SCRIPT_PATH = /^(\/[A-Za-z0-9_-]+)+\/(challenge|captcha|jsapi)\.js$/;
61
+
62
+ const DATADOME_SUBMIT = new Set([
63
+ 'method', 'captcha_url', 'pageurl', 'useragent', 'json', 'proxy', 'proxytype',
64
+ ]);
65
+
66
+ // Every DataDome challenge is served from this domain or a subdomain of it
67
+ // (geo.captcha-delivery.com in practice). CapSkip refuses any other host.
68
+ const DATADOME_CHALLENGE_DOMAIN = 'captcha-delivery.com';
69
+
70
+ // The two challenge pages: /captcha/ draws the slider, /interstitial/ runs the
71
+ // device check. Any other path renders nothing to solve.
72
+ const DATADOME_CHALLENGE_PATHS = ['/captcha', '/interstitial'];
73
+
74
+ // A Chrome version as Chrome writes it in its User-Agent: four dotted numbers.
75
+ const CHROME_VERSION = /\bChrome\/(\d+)\.\d+\.\d+\.\d+\b/;
76
+
77
+ // Chrome-derived browsers and non-desktop builds. A User-Agent carrying any of
78
+ // these is not stock Windows Chrome, so a cookie earned under it would be
79
+ // refused on replay; the server answers ERROR_WRONG_USERAGENT for each.
80
+ const NOT_STOCK_WINDOWS_CHROME = [
81
+ 'Edg/', 'EdgA/', 'OPR/', 'YaBrowser/', 'SamsungBrowser/', 'Vivaldi/',
82
+ 'HeadlessChrome/', 'CriOS/', 'Mobile', 'Android',
83
+ ];
84
+
49
85
  // CapSkip solves the puzzle family only. `avatar` is a different challenge
50
86
  // behind a different endpoint; the server refuses it at submit time rather than
51
87
  // answering it with a puzzle answer, which would bill for a solve the target
@@ -81,6 +117,16 @@ const PARAM_ALIASES = {
81
117
  moduleScript: 'module_script',
82
118
  nomoduleScript: 'nomodule_script',
83
119
  noModuleScript: 'nomodule_script',
120
+ // AWS WAF: the API documents challenge_script / captcha_script in snake case
121
+ // and jsapiScript in camel case, as 2Captcha does. Accept the other spelling
122
+ // of each, which is what createTask-style callers arrive with.
123
+ challengeScript: 'challenge_script',
124
+ captchaScript: 'captcha_script',
125
+ jsapi_script: 'jsapiScript',
126
+ // DataDome: 2Captcha and the API document captcha_url; createTask-style
127
+ // callers arrive with captchaUrl.
128
+ captchaUrl: 'captcha_url',
129
+ captchaURL: 'captcha_url',
84
130
  };
85
131
 
86
132
  function has(obj, key) {
@@ -353,6 +399,255 @@ function validateFriendlyCaptchaSubmit(params) {
353
399
  }
354
400
  }
355
401
 
402
+ /**
403
+ * True for a script URL CapSkip will accept on an AWS WAF task.
404
+ *
405
+ * Mirrors the server: https, a host under .awswaf.com, no credentials or
406
+ * explicit port, and a path ending in challenge.js, captcha.js or jsapi.js.
407
+ * The server only applies the check once the task is queued, so failing here
408
+ * saves a submit that could never solve.
409
+ */
410
+ function isAmazonWafScriptUrl(url) {
411
+ let parsed;
412
+ try {
413
+ parsed = new URL(String(url).trim());
414
+ } catch (error) {
415
+ return false;
416
+ }
417
+ // WHATWG URL already lowercases the scheme and host, and drops a :443 on an
418
+ // https URL, so `port` is empty for both the default and an explicit 443.
419
+ return parsed.protocol === 'https:'
420
+ && parsed.hostname.toLowerCase().endsWith('.awswaf.com')
421
+ && parsed.username === ''
422
+ && parsed.password === ''
423
+ && (parsed.port === '' || parsed.port === '443')
424
+ && AMAZON_WAF_SCRIPT_PATH.test(parsed.pathname);
425
+ }
426
+
427
+ /**
428
+ * Drop unset optionals, and the empty ones too: an AWS WAF optional left as ''
429
+ * means "not sent" to the server, exactly like an absent one, so it is dropped
430
+ * rather than tripping the iv/context pairing.
431
+ */
432
+ function normalizeAmazonWafSubmit(params) {
433
+ const out = {};
434
+ for (const [key, value] of Object.entries(dropUnset(params))) {
435
+ if (value !== '') {
436
+ out[key] = value;
437
+ }
438
+ }
439
+ return out;
440
+ }
441
+
442
+ function validateAmazonWafSubmit(params) {
443
+ // Without sitekey the server answers ERROR_BAD_PARAMETERS and without
444
+ // pageurl ERROR_PAGEURL. Fail locally so a missing value costs no round-trip.
445
+ for (const key of ['sitekey', 'pageurl']) {
446
+ if (!params[key]) {
447
+ throw new ValidationException(`'${key}' is required for Amazon WAF.`);
448
+ }
449
+ }
450
+
451
+ const pageurl = String(params.pageurl).trim();
452
+ if (!/^https?:\/\//i.test(pageurl)) {
453
+ throw new ValidationException(
454
+ `'pageurl' must be a full http or https URL for Amazon WAF, got '${pageurl}'.`,
455
+ );
456
+ }
457
+
458
+ // iv and context come off the same challenge page and only work as a pair;
459
+ // the server refuses one without the other with ERROR_BAD_PARAMETERS.
460
+ if (Boolean(params.iv) !== Boolean(params.context)) {
461
+ throw new ValidationException(
462
+ "Amazon WAF 'iv' and 'context' go together: send both (a challenge "
463
+ + 'page) or neither (a widget page, or let CapSkip read them from pageurl).',
464
+ );
465
+ }
466
+
467
+ for (const key of AMAZON_WAF_SCRIPTS) {
468
+ const value = params[key];
469
+ if (value && !isAmazonWafScriptUrl(value)) {
470
+ throw new ValidationException(
471
+ `'${key}' must be the https URL of an AWS WAF script on an awswaf.com `
472
+ + `host (challenge.js, captcha.js or jsapi.js), got '${value}'. CapSkip `
473
+ + 'refuses any other script, because it is not an AWS WAF integration.',
474
+ );
475
+ }
476
+ }
477
+
478
+ const unknown = unknownKeys(params, AMAZON_WAF_SUBMIT);
479
+ if (unknown.length > 0) {
480
+ throw new ValidationException(
481
+ `Unsupported parameters for Amazon WAF: ${reprList(unknown)}.`,
482
+ );
483
+ }
484
+ }
485
+
486
+ const HTML_NAMED_ENTITIES = {
487
+ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
488
+ };
489
+
490
+ /**
491
+ * Decode HTML character references, as the server does before it parses a
492
+ * DataDome captcha_url: an iframe src copied out of the page source carries
493
+ * &amp; between its query parameters.
494
+ */
495
+ function htmlUnescape(text) {
496
+ return text.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[A-Za-z]+);/g, (match, ref) => {
497
+ if (ref[0] === '#') {
498
+ const code = ref[1] === 'x' || ref[1] === 'X'
499
+ ? parseInt(ref.slice(2), 16)
500
+ : parseInt(ref.slice(1), 10);
501
+ try {
502
+ return String.fromCodePoint(code);
503
+ } catch (error) {
504
+ return match;
505
+ }
506
+ }
507
+ const named = HTML_NAMED_ENTITIES[ref.toLowerCase()];
508
+ return named === undefined ? match : named;
509
+ });
510
+ }
511
+
512
+ function isDataDomeHost(host) {
513
+ const lowered = String(host || '').toLowerCase();
514
+ return lowered === DATADOME_CHALLENGE_DOMAIN
515
+ || lowered.endsWith(`.${DATADOME_CHALLENGE_DOMAIN}`);
516
+ }
517
+
518
+ /**
519
+ * The query of a DataDome challenge URL as a URLSearchParams, or null if it is
520
+ * not one.
521
+ *
522
+ * Mirrors the server: an https URL on captcha-delivery.com (or a subdomain),
523
+ * path /captcha/ or /interstitial/, carrying initialCid, hash and cid. An
524
+ * iframe src copied HTML-escaped (&amp;) and a scheme-relative //geo... URL
525
+ * are both read the way the server reads them.
526
+ */
527
+ function parseDataDomeCaptchaUrl(url) {
528
+ let text = htmlUnescape(String(url === undefined || url === null ? '' : url).trim());
529
+ if (text.startsWith('//')) {
530
+ text = `https:${text}`;
531
+ }
532
+ let parsed;
533
+ try {
534
+ parsed = new URL(text);
535
+ } catch (error) {
536
+ return null;
537
+ }
538
+ if (parsed.protocol !== 'https:' || !isDataDomeHost(parsed.hostname)) {
539
+ return null;
540
+ }
541
+ const pathname = parsed.pathname.replace(/\/+$/, '').toLowerCase();
542
+ if (!DATADOME_CHALLENGE_PATHS.includes(pathname)) {
543
+ return null;
544
+ }
545
+ const query = parsed.searchParams;
546
+ if (!['initialCid', 'hash', 'cid'].every((key) => query.get(key))) {
547
+ return null;
548
+ }
549
+ return query;
550
+ }
551
+
552
+ /**
553
+ * True for a stock Chrome-for-Windows User-Agent, the only kind CapSkip takes.
554
+ *
555
+ * DataDome ties the cookie to the browser that earned it, and CapSkip solves
556
+ * in Chrome on Windows, so a cookie for any other User-Agent would be refused
557
+ * on replay. The server also requires the same Chrome major version as its
558
+ * own browser; that part can only be checked there.
559
+ */
560
+ function isDataDomeUserAgent(userAgent) {
561
+ const ua = String(userAgent === undefined || userAgent === null ? '' : userAgent);
562
+ return ua.includes('Windows NT ')
563
+ && CHROME_VERSION.test(ua)
564
+ && !NOT_STOCK_WINDOWS_CHROME.some((token) => ua.includes(token));
565
+ }
566
+
567
+ /**
568
+ * Drop unset optionals, and the empty ones too: a DataDome optional left as ''
569
+ * means "not sent" to the server, exactly like an absent one.
570
+ */
571
+ function normalizeDataDomeSubmit(params) {
572
+ const out = {};
573
+ for (const [key, value] of Object.entries(dropUnset(params))) {
574
+ if (value !== '') {
575
+ out[key] = value;
576
+ }
577
+ }
578
+ return out;
579
+ }
580
+
581
+ /** An absolute http(s) URL with a host that is not DataDome's own. */
582
+ function isDataDomePageUrl(pageurl) {
583
+ let parsed;
584
+ try {
585
+ parsed = new URL(pageurl);
586
+ } catch (error) {
587
+ return false;
588
+ }
589
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:')
590
+ && parsed.hostname !== ''
591
+ && !isDataDomeHost(parsed.hostname);
592
+ }
593
+
594
+ function validateDataDomeSubmit(params) {
595
+ // Without captcha_url the server answers ERROR_BAD_PARAMETERS and without
596
+ // pageurl ERROR_PAGEURL. Fail locally so a missing value costs no round-trip.
597
+ for (const key of ['captcha_url', 'pageurl']) {
598
+ if (!params[key]) {
599
+ throw new ValidationException(`'${key}' is required for DataDome.`);
600
+ }
601
+ }
602
+
603
+ const query = parseDataDomeCaptchaUrl(params.captcha_url);
604
+ if (query === null) {
605
+ throw new ValidationException(
606
+ "'captcha_url' must be the https address of a DataDome challenge on "
607
+ + 'captcha-delivery.com, under /captcha/ or /interstitial/ and carrying '
608
+ + `initialCid, hash and cid, got '${params.captcha_url}'. Take it from `
609
+ + "the block page's iframe src, or build it from the page's `var dd` object.",
610
+ );
611
+ }
612
+ // t=bv is DataDome saying the address that fetched the block page is
613
+ // banned. Nothing sent from it can pass, and the server refuses the task
614
+ // with ERROR_PROXY_BANNED, so say so before the round-trip.
615
+ if (query.get('t') === 'bv') {
616
+ throw new ValidationException(
617
+ 'This DataDome block page bans the IP address that requested it '
618
+ + '(t=bv), so no solve from that address can pass. Change the address '
619
+ + '(proxy), request the page again and submit the new captcha_url.',
620
+ );
621
+ }
622
+
623
+ const pageurl = String(params.pageurl).trim();
624
+ if (!isDataDomePageUrl(pageurl)) {
625
+ throw new ValidationException(
626
+ "'pageurl' must be the full http or https URL of the page that "
627
+ + `returned the DataDome block page, got '${pageurl}'.`,
628
+ );
629
+ }
630
+
631
+ const userAgent = params.useragent;
632
+ if (userAgent !== undefined && userAgent !== null && userAgent !== ''
633
+ && !isDataDomeUserAgent(userAgent)) {
634
+ throw new ValidationException(
635
+ "DataDome 'userAgent' must be a stock Chrome for Windows User-Agent "
636
+ + 'with the same major version as the browser CapSkip solves with, got '
637
+ + `'${userAgent}'. The cookie only works with the User-Agent that `
638
+ + "earned it. Leave it out and CapSkip uses its own browser's, which "
639
+ + 'the result reports as `userAgent`.',
640
+ );
641
+ }
642
+
643
+ const unknown = unknownKeys(params, DATADOME_SUBMIT);
644
+ if (unknown.length > 0) {
645
+ throw new ValidationException(
646
+ `Unsupported parameters for DataDome: ${reprList(unknown)}.`,
647
+ );
648
+ }
649
+ }
650
+
356
651
  function validateProxyType(params) {
357
652
  const proxytype = params.proxytype;
358
653
  if (proxytype === undefined || proxytype === null || proxytype === '') {
@@ -390,6 +685,12 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
390
685
  } else if (captchaType === 'friendly_captcha') {
391
686
  prepared = dropUnset(prepared);
392
687
  validateFriendlyCaptchaSubmit(prepared);
688
+ } else if (captchaType === 'amazon_waf') {
689
+ prepared = normalizeAmazonWafSubmit(prepared);
690
+ validateAmazonWafSubmit(prepared);
691
+ } else if (captchaType === 'datadome') {
692
+ prepared = normalizeDataDomeSubmit(prepared);
693
+ validateDataDomeSubmit(prepared);
393
694
  }
394
695
 
395
696
  // Skipped for 'normal', which rejects proxy outright with a clearer message.
@@ -410,6 +711,11 @@ module.exports = {
410
711
  CAPY_SUBMIT,
411
712
  CAPTCHAFOX_SUBMIT,
412
713
  FRIENDLY_CAPTCHA_SUBMIT,
714
+ AMAZON_WAF_SUBMIT,
715
+ AMAZON_WAF_SCRIPTS,
716
+ DATADOME_SUBMIT,
717
+ DATADOME_CHALLENGE_DOMAIN,
718
+ DATADOME_CHALLENGE_PATHS,
413
719
  CAPY_VERSIONS,
414
720
  FRIENDLY_CAPTCHA_VERSIONS,
415
721
  PROXY_TYPES,
@@ -426,6 +732,14 @@ module.exports = {
426
732
  validateCapySubmit,
427
733
  validateCaptchaFoxSubmit,
428
734
  validateFriendlyCaptchaSubmit,
735
+ isAmazonWafScriptUrl,
736
+ normalizeAmazonWafSubmit,
737
+ validateAmazonWafSubmit,
738
+ htmlUnescape,
739
+ parseDataDomeCaptchaUrl,
740
+ isDataDomeUserAgent,
741
+ normalizeDataDomeSubmit,
742
+ validateDataDomeSubmit,
429
743
  validateProxyType,
430
744
  prepareSubmitParams,
431
745
  };
package/src/index.js CHANGED
@@ -27,5 +27,5 @@ module.exports = {
27
27
  NetworkException,
28
28
  ApiException,
29
29
  TimeoutException,
30
- version: '1.1.0',
30
+ version: '1.5.0',
31
31
  };