is-antibot 1.3.5 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/src/index.js +156 -121
package/README.md CHANGED
@@ -67,14 +67,14 @@ const isAntibot = require('is-antibot')
67
67
  const response = await fetch('https://www.linkedin.com/in/kikobeats/')
68
68
  const html = await response.text()
69
69
 
70
- const { detected, provider } = isAntibot({
70
+ const { detected, provider, detection } = isAntibot({
71
71
  headers: response.headers,
72
72
  html,
73
73
  url: response.url
74
74
  })
75
75
 
76
76
  if (detected) {
77
- console.log(`Antibot detected: ${provider}`)
77
+ console.log(`Antibot detected: ${provider} via ${detection}`)
78
78
  }
79
79
  ```
80
80
 
@@ -84,10 +84,10 @@ It also works with [got](https://github.com/sindresorhus/got) or any library whe
84
84
  const response = await got('https://www.linkedin.com/in/kikobeats/')
85
85
  .catch(error => errorresponse)
86
86
 
87
- const { detected, provider } = isAntibot(response)
87
+ const { detected, provider, detection } = isAntibot(response)
88
88
 
89
89
  if (detected) {
90
- console.log(`Antibot detected: ${provider}`)
90
+ console.log(`Antibot detected: ${provider} via ${detection}`)
91
91
  }
92
92
  ```
93
93
 
@@ -95,6 +95,7 @@ The library returns an object with the following properties:
95
95
 
96
96
  - `detected` (boolean): Whether an antibot challenge was detected
97
97
  - `provider` (string|null): The name of the detected provider (e.g., 'cloudflare', 'recaptcha')
98
+ - `detection` (string|null): Where the signal came from: `'headers'`, `'cookies'`, `'html'`, or `'url'`
98
99
 
99
100
  ## License
100
101
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "is-antibot",
3
3
  "description": "Identify if a response is an antibot challenge from CloudFlare, Akamai, DataDome, Vercel, PerimeterX, Shape Security, and more, including CAPTCHA providers like reCAPTCHA and hCaptcha.",
4
4
  "homepage": "https://github.com/microlinkhq/is-antibot",
5
- "version": "1.3.5",
5
+ "version": "1.4.0",
6
6
  "exports": {
7
7
  ".": "./src/index.js"
8
8
  },
package/src/index.js CHANGED
@@ -3,6 +3,13 @@
3
3
  const { splitSetCookieString } = require('cookie-es')
4
4
  const debug = require('debug-logfmt')('is-antibot')
5
5
 
6
+ const DETECTION = {
7
+ HEADERS: 'headers',
8
+ COOKIES: 'cookies',
9
+ HTML: 'html',
10
+ URL: 'url'
11
+ }
12
+
6
13
  const createGetHeader = headers =>
7
14
  typeof headers.get === 'function'
8
15
  ? name => headers.get(name)
@@ -12,6 +19,14 @@ const createTestPattern = value => {
12
19
  if (!value) return () => false
13
20
  const lowerValue = value.toLowerCase()
14
21
  return (pattern, isRegex = false) => {
22
+ if (pattern instanceof RegExp) {
23
+ try {
24
+ return pattern.test(value)
25
+ } catch {
26
+ return false
27
+ }
28
+ }
29
+
15
30
  if (isRegex) {
16
31
  try {
17
32
  return new RegExp(pattern, 'i').test(value)
@@ -23,9 +38,9 @@ const createTestPattern = value => {
23
38
  }
24
39
  }
25
40
 
26
- const createResult = (detected, provider) => {
27
- debug({ detected, provider })
28
- return { detected, provider }
41
+ const createResult = (detected, provider, detection = null) => {
42
+ debug({ detected, provider, detection })
43
+ return { detected, provider, detection }
29
44
  }
30
45
 
31
46
  const createHasCookie = headers => {
@@ -36,344 +51,364 @@ const createHasCookie = headers => {
36
51
  )
37
52
  }
38
53
 
54
+ const getHeaderNames = headers =>
55
+ typeof headers.keys === 'function'
56
+ ? Array.from(headers.keys())
57
+ : Object.keys(headers)
58
+
39
59
  const detect = ({ headers = {}, html = '', url = '' } = {}) => {
40
60
  const getHeader = createGetHeader(headers)
41
61
  const hasCookie = createHasCookie(headers)
42
62
  const htmlHas = createTestPattern(html)
43
63
  const urlHas = createTestPattern(url)
44
64
 
65
+ const hasAnyHeader = headerNames =>
66
+ headerNames.some(headerName => getHeader(headerName))
67
+
68
+ const hasAnyCookie = cookieNames =>
69
+ cookieNames.some(cookieName => hasCookie(cookieName))
70
+
71
+ const hasAnyHtml = patterns => patterns.some(pattern => htmlHas(pattern))
72
+
73
+ const hasAnyUrl = patterns => patterns.some(pattern => urlHas(pattern))
74
+
75
+ const byHeaders = provider => createResult(true, provider, DETECTION.HEADERS)
76
+
77
+ const byCookies = provider => createResult(true, provider, DETECTION.COOKIES)
78
+
79
+ const byHtml = provider => createResult(true, provider, DETECTION.HTML)
80
+
81
+ const byUrl = provider => createResult(true, provider, DETECTION.URL)
82
+
45
83
  // CloudFlare: Check for cf-mitigated header with 'challenge' value
46
84
  // Official docs: https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/
47
85
  if (getHeader('cf-mitigated') === 'challenge') {
48
- return createResult(true, 'cloudflare')
86
+ return byHeaders('cloudflare')
49
87
  }
50
88
 
51
89
  // Cloudflare: cf_clearance cookie indicates Cloudflare challenge flow
52
- if (hasCookie('cf_clearance=')) {
53
- return createResult(true, 'cloudflare')
90
+ if (hasAnyCookie(['cf_clearance='])) {
91
+ return byCookies('cloudflare')
54
92
  }
55
93
 
56
94
  // Vercel: Check for x-vercel-mitigated header with 'challenge' value
57
95
  // Solver reference: https://github.com/glizzykingdreko/Vercel-Attack-Mode-Solver
58
96
  if (getHeader('x-vercel-mitigated') === 'challenge') {
59
- return createResult(true, 'vercel')
97
+ return byHeaders('vercel')
60
98
  }
61
99
 
62
100
  // Akamai: Check for akamai-cache-status header starting with 'Error'
63
101
  // Official docs: https://techdocs.akamai.com/property-mgr/docs/return-cache-status
64
102
  if (getHeader('akamai-cache-status')?.startsWith('Error')) {
65
- return createResult(true, 'akamai')
103
+ return byHeaders('akamai')
66
104
  }
67
105
 
68
106
  // Akamai: Check for additional identifying headers (akamai-grn, x-akamai-session-info)
69
107
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-akamai.json
70
- if (getHeader('akamai-grn') || getHeader('x-akamai-session-info')) {
71
- return createResult(true, 'akamai')
108
+ if (hasAnyHeader(['akamai-grn', 'x-akamai-session-info'])) {
109
+ return byHeaders('akamai')
72
110
  }
73
111
 
74
112
  // Akamai: _abck bot manager tracking cookie
75
- if (hasCookie('_abck=')) {
76
- return createResult(true, 'akamai')
113
+ if (hasAnyCookie(['_abck='])) {
114
+ return byCookies('akamai')
77
115
  }
78
116
 
79
117
  // Akamai: Bot Manager API namespace (bmak) in html
80
- if (htmlHas('bmak.')) {
81
- return createResult(true, 'akamai')
118
+ if (hasAnyHtml(['bmak.'])) {
119
+ return byHtml('akamai')
82
120
  }
83
121
 
84
122
  // DataDome: Check for x-dd-b header with values '1' (soft challenge) or '2' (hard challenge/CAPTCHA)
85
123
  // Official docs: https://docs.datadome.co/reference/validate-request
86
124
  if (['1', '2'].includes(getHeader('x-dd-b'))) {
87
- return createResult(true, 'datadome')
125
+ return byHeaders('datadome')
88
126
  }
89
127
 
90
128
  // DataDome: Check for x-datadome or x-datadome-cid header presence
91
129
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-datadome.json
92
- if (getHeader('x-datadome') || getHeader('x-datadome-cid')) {
93
- return createResult(true, 'datadome')
130
+ if (hasAnyHeader(['x-datadome', 'x-datadome-cid'])) {
131
+ return byHeaders('datadome')
94
132
  }
95
133
 
96
134
  // DataDome: datadome tracking cookie
97
- if (hasCookie('datadome=')) {
98
- return createResult(true, 'datadome')
135
+ if (hasAnyCookie(['datadome='])) {
136
+ return byCookies('datadome')
99
137
  }
100
138
 
101
139
  // PerimeterX: Check for X-PX-Authorization header (primary indicator)
102
140
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-perimeterx.json
103
141
  if (getHeader('x-px-authorization')) {
104
- return createResult(true, 'perimeterx')
142
+ return byHeaders('perimeterx')
105
143
  }
106
144
 
107
145
  // PerimeterX: Check for window._pxAppId, pxInit, or _pxAction in html
108
146
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-perimeterx.json
109
- if (htmlHas('window._pxAppId') || htmlHas('pxInit') || htmlHas('_pxAction')) {
110
- return createResult(true, 'perimeterx')
147
+ if (hasAnyHtml(['window._pxAppId', 'pxInit', '_pxAction'])) {
148
+ return byHtml('perimeterx')
111
149
  }
112
150
 
113
151
  // PerimeterX: _px3 or _pxhd cookies
114
- if (hasCookie('_px3=') || hasCookie('_pxhd=')) {
115
- return createResult(true, 'perimeterx')
152
+ if (hasAnyCookie(['_px3=', '_pxhd='])) {
153
+ return byCookies('perimeterx')
116
154
  }
117
155
 
118
156
  // Shape Security: Check for dynamic header patterns x-[8chars]-[abcdfz]
119
157
  // These headers use 8 random characters followed by suffixes like -a, -b, -c, -d, -f, or -z
120
158
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
121
- const headerNames = Object.keys(headers)
159
+ const headerNames = getHeaderNames(headers)
122
160
  for (const name of headerNames) {
123
161
  if (/^x-[a-z0-9]{8}-[abcdfz]$/i.test(name)) {
124
- return createResult(true, 'shapesecurity')
162
+ return byHeaders('shapesecurity')
125
163
  }
126
164
  }
127
165
 
128
166
  // Shape Security: Check for 'shapesecurity' text in response html
129
167
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
130
- if (htmlHas('shapesecurity')) {
131
- return createResult(true, 'shapesecurity')
168
+ if (hasAnyHtml(['shapesecurity'])) {
169
+ return byHtml('shapesecurity')
132
170
  }
133
171
 
134
172
  // Kasada: Check for x-kasada or x-kasada-challenge headers
135
173
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-kasada.json
136
- if (getHeader('x-kasada') || getHeader('x-kasada-challenge')) {
137
- return createResult(true, 'kasada')
174
+ if (hasAnyHeader(['x-kasada', 'x-kasada-challenge'])) {
175
+ return byHeaders('kasada')
138
176
  }
139
177
 
140
178
  // Kasada: Check for __kasada global object or kasada.js script in html
141
179
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-kasada.json
142
- if (htmlHas('__kasada') || htmlHas('kasada.js')) {
143
- return createResult(true, 'kasada')
180
+ if (hasAnyHtml(['__kasada', 'kasada.js'])) {
181
+ return byHtml('kasada')
144
182
  }
145
183
 
146
184
  // Imperva/Incapsula: Check for x-cdn header with 'Incapsula' value or x-iinfo header
147
185
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
148
- if (getHeader('x-cdn') === 'Incapsula' || getHeader('x-iinfo')) {
149
- return createResult(true, 'imperva')
186
+ if (getHeader('x-cdn') === 'Incapsula' || hasAnyHeader(['x-iinfo'])) {
187
+ return byHeaders('imperva')
150
188
  }
151
189
 
152
190
  // Imperva/Incapsula: Check for 'incapsula' or 'imperva' text in response html
153
191
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
154
- if (htmlHas('incapsula') || htmlHas('imperva')) {
155
- return createResult(true, 'imperva')
192
+ if (hasAnyHtml(['incapsula', 'imperva'])) {
193
+ return byHtml('imperva')
156
194
  }
157
195
 
158
196
  // Imperva/Incapsula: incap_ses_, visid_incap_, or reese84 cookies
159
197
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
160
- if (
161
- hasCookie('incap_ses_') ||
162
- hasCookie('visid_incap_') ||
163
- hasCookie('reese84=')
164
- ) {
165
- return createResult(true, 'imperva')
198
+ if (hasAnyCookie(['incap_ses_', 'visid_incap_', 'reese84='])) {
199
+ return byCookies('imperva')
166
200
  }
167
201
 
168
202
  // Reblaze: rbzid or rbzsessionid cookies
169
203
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-reblaze.json
170
- if (hasCookie('rbzid=') || hasCookie('rbzsessionid=')) {
171
- return createResult(true, 'reblaze')
204
+ if (hasAnyCookie(['rbzid=', 'rbzsessionid='])) {
205
+ return byCookies('reblaze')
172
206
  }
173
207
 
174
208
  // Reblaze: Check for 'reblaze' text in response html
175
- if (htmlHas('reblaze')) {
176
- return createResult(true, 'reblaze')
209
+ if (hasAnyHtml(['reblaze'])) {
210
+ return byHtml('reblaze')
177
211
  }
178
212
 
179
213
  // Cheq: Check for CheqSdk or cheqzone.com in html
180
214
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-cheq.json
181
- if (htmlHas('CheqSdk') || htmlHas('cheqzone.com')) {
182
- return createResult(true, 'cheq')
215
+ if (hasAnyHtml(['CheqSdk', 'cheqzone.com'])) {
216
+ return byHtml('cheq')
183
217
  }
184
218
 
185
219
  // Cheq: Check for cheqzone.com or cheq.ai in URL
186
- if (urlHas('cheqzone\\.com', true) || urlHas('cheq\\.ai', true)) {
187
- return createResult(true, 'cheq')
220
+ if (hasAnyUrl([/cheqzone\.com/i, /cheq\.ai/i])) {
221
+ return byUrl('cheq')
188
222
  }
189
223
 
190
224
  // Sucuri: Check for 'sucuri' text in response html
191
225
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-sucuri.json
192
- if (htmlHas('sucuri')) {
193
- return createResult(true, 'sucuri')
226
+ if (hasAnyHtml(['sucuri'])) {
227
+ return byHtml('sucuri')
194
228
  }
195
229
 
196
230
  // ThreatMetrix: Check for 'ThreatMetrix' in html
197
231
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-threatmetrix.json
198
- if (htmlHas('ThreatMetrix')) {
199
- return createResult(true, 'threatmetrix')
232
+ if (hasAnyHtml(['ThreatMetrix'])) {
233
+ return byHtml('threatmetrix')
200
234
  }
201
235
 
202
236
  // ThreatMetrix: Check for fp/check.js fingerprint endpoint in URL
203
- if (urlHas('fp/check.js')) {
204
- return createResult(true, 'threatmetrix')
237
+ if (hasAnyUrl(['fp/check.js'])) {
238
+ return byUrl('threatmetrix')
205
239
  }
206
240
 
207
241
  // Meetrics: Check for 'meetrics' text in response html
208
242
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-meetrics.json
209
- if (htmlHas('meetrics')) {
210
- return createResult(true, 'meetrics')
243
+ if (hasAnyHtml(['meetrics'])) {
244
+ return byHtml('meetrics')
211
245
  }
212
246
 
213
247
  // Meetrics: Check for meetrics.com in URL
214
- if (urlHas('meetrics\\.com', true)) {
215
- return createResult(true, 'meetrics')
248
+ if (hasAnyUrl([/meetrics\.com/i])) {
249
+ return byUrl('meetrics')
216
250
  }
217
251
 
218
252
  // Ocule: Check for ocule.co.uk in html
219
253
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-ocule.json
220
- if (htmlHas('ocule.co.uk')) {
221
- return createResult(true, 'ocule')
254
+ if (hasAnyHtml(['ocule.co.uk'])) {
255
+ return byHtml('ocule')
222
256
  }
223
257
 
224
258
  // Ocule: Check for ocule.co.uk in URL
225
- if (urlHas('ocule\\.co\\.uk', true)) {
226
- return createResult(true, 'ocule')
259
+ if (hasAnyUrl([/ocule\.co\.uk/i])) {
260
+ return byUrl('ocule')
227
261
  }
228
262
 
229
263
  // reCAPTCHA: Check for recaptcha/api, google.com/recaptcha, gstatic.com/recaptcha, or recaptcha.net in URL
230
264
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
231
265
  if (
232
- urlHas('recaptcha/api') ||
233
- urlHas('google\\.com/recaptcha', true) ||
234
- urlHas('gstatic.com/recaptcha') ||
235
- urlHas('recaptcha.net')
266
+ hasAnyUrl(['recaptcha/api', 'gstatic.com/recaptcha', 'recaptcha.net']) ||
267
+ hasAnyUrl([/google\.com\/recaptcha/i])
236
268
  ) {
237
- return createResult(true, 'recaptcha')
269
+ return byUrl('recaptcha')
238
270
  }
239
271
 
240
- // reCAPTCHA: Check for grecaptcha global object in html (primary JavaScript indicator)
241
- // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
242
- if (htmlHas('grecaptcha')) {
243
- return createResult(true, 'recaptcha')
272
+ // reCAPTCHA: Check for grecaptcha API usage in html (JavaScript indicator)
273
+ // Note: plain "grecaptcha" is too broad (e.g. ".grecaptcha-badge" CSS appears on normal YouTube pages)
274
+ if (
275
+ hasAnyHtml([
276
+ /\b(?:window\.)?grecaptcha\s*\.(?:execute|render|ready|getResponse|enterprise)\b/i,
277
+ /\b(?:window\.)?grecaptcha\s*\(/i,
278
+ /\b__grecaptcha_cfg\b/i
279
+ ])
280
+ ) {
281
+ return byHtml('recaptcha')
244
282
  }
245
283
 
246
284
  // reCAPTCHA: Check for g-recaptcha container class in html
247
285
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
248
- if (htmlHas('g-recaptcha')) {
249
- return createResult(true, 'recaptcha')
286
+ if (hasAnyHtml(['g-recaptcha'])) {
287
+ return byHtml('recaptcha')
250
288
  }
251
289
 
252
290
  // hCaptcha: Check for hcaptcha.com domain in URL
253
291
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
254
- if (urlHas('hcaptcha\\.com', true)) {
255
- return createResult(true, 'hcaptcha')
292
+ if (hasAnyUrl([/hcaptcha\.com/i])) {
293
+ return byUrl('hcaptcha')
256
294
  }
257
295
 
258
296
  // hCaptcha: Check for hcaptcha.com API domain or h-captcha container class in html
259
297
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
260
298
  // Note: bare 'hcaptcha' matches too broadly (could appear in articles discussing hCaptcha)
261
- if (htmlHas('hcaptcha.com') || htmlHas('h-captcha')) {
262
- return createResult(true, 'hcaptcha')
299
+ if (hasAnyHtml(['hcaptcha.com', 'h-captcha'])) {
300
+ return byHtml('hcaptcha')
263
301
  }
264
302
 
265
303
  // FunCaptcha (Arkose Labs): Check for arkoselabs.com or funcaptcha in URL
266
304
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
267
- if (urlHas('arkoselabs\\.com', true) || urlHas('funcaptcha')) {
268
- return createResult(true, 'funcaptcha')
305
+ if (hasAnyUrl([/arkoselabs\.com/i]) || hasAnyUrl(['funcaptcha'])) {
306
+ return byUrl('funcaptcha')
269
307
  }
270
308
 
271
309
  // FunCaptcha (Arkose Labs): Check for arkoselabs.com API domain or funcaptcha in html
272
310
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
273
311
  // Note: bare 'arkose' matches too broadly (e.g. Facebook bundles Arkose SDK for login without blocking content)
274
- if (htmlHas('arkoselabs.com') || htmlHas('funcaptcha')) {
275
- return createResult(true, 'funcaptcha')
312
+ if (hasAnyHtml(['arkoselabs.com', 'funcaptcha'])) {
313
+ return byHtml('funcaptcha')
276
314
  }
277
315
 
278
316
  // GeeTest: Check for geetest.com domain in URL
279
317
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
280
- if (urlHas('geetest\\.com', true)) {
281
- return createResult(true, 'geetest')
318
+ if (hasAnyUrl([/geetest\.com/i])) {
319
+ return byUrl('geetest')
282
320
  }
283
321
 
284
322
  // GeeTest: Check for geetest object or text in html
285
323
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
286
324
  // Note: bare 'gt.js' removed (too generic, any script named gt.js would match)
287
- if (htmlHas('geetest')) {
288
- return createResult(true, 'geetest')
325
+ if (hasAnyHtml(['geetest'])) {
326
+ return byHtml('geetest')
289
327
  }
290
328
 
291
329
  // Cloudflare Turnstile: Check for challenges.cloudflare.com/turnstile in URL
292
- if (urlHas('challenges\\.cloudflare\\.com/turnstile', true)) {
293
- return createResult(true, 'cloudflare-turnstile')
330
+ if (hasAnyUrl([/challenges\.cloudflare\.com\/turnstile/i])) {
331
+ return byUrl('cloudflare-turnstile')
294
332
  }
295
333
 
296
334
  // Cloudflare Turnstile: Check for cf-turnstile class or turnstile API script in html
297
335
  // Note: bare 'turnstile' matches too broadly (common English word)
298
- if (
299
- htmlHas('cf-turnstile') ||
300
- htmlHas('challenges.cloudflare.com/turnstile')
301
- ) {
302
- return createResult(true, 'cloudflare-turnstile')
336
+ if (hasAnyHtml(['cf-turnstile', 'challenges.cloudflare.com/turnstile'])) {
337
+ return byHtml('cloudflare-turnstile')
303
338
  }
304
339
 
305
340
  // Friendly Captcha: Check for friendlycaptcha.com in URL
306
341
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-friendlycaptcha.json
307
- if (urlHas('friendlycaptcha\\.com', true)) {
308
- return createResult(true, 'friendly-captcha')
342
+ if (hasAnyUrl([/friendlycaptcha\.com/i])) {
343
+ return byUrl('friendly-captcha')
309
344
  }
310
345
 
311
346
  // Friendly Captcha: Check for frc-captcha container or friendlyChallenge object in html
312
- if (htmlHas('frc-captcha') || htmlHas('friendlyChallenge')) {
313
- return createResult(true, 'friendly-captcha')
347
+ if (hasAnyHtml(['frc-captcha', 'friendlyChallenge'])) {
348
+ return byHtml('friendly-captcha')
314
349
  }
315
350
 
316
351
  // Captcha.eu: Check for captcha.eu in URL
317
352
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-captchaeu.json
318
- if (urlHas('captcha\\.eu', true)) {
319
- return createResult(true, 'captcha-eu')
353
+ if (hasAnyUrl([/captcha\.eu/i])) {
354
+ return byUrl('captcha-eu')
320
355
  }
321
356
 
322
357
  // Captcha.eu: Check for CaptchaEU or captchaeu in html
323
- if (htmlHas('CaptchaEU') || htmlHas('captchaeu')) {
324
- return createResult(true, 'captcha-eu')
358
+ if (hasAnyHtml(['CaptchaEU', 'captchaeu'])) {
359
+ return byHtml('captcha-eu')
325
360
  }
326
361
 
327
362
  // QCloud Captcha (Tencent): Check for turing.captcha.qcloud.com in URL
328
363
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-qcloud.json
329
- if (urlHas('turing\\.captcha\\.qcloud\\.com', true)) {
330
- return createResult(true, 'qcloud-captcha')
364
+ if (hasAnyUrl([/turing\.captcha\.qcloud\.com/i])) {
365
+ return byUrl('qcloud-captcha')
331
366
  }
332
367
 
333
368
  // QCloud Captcha: Check for TencentCaptcha or turing.captcha in html
334
- if (htmlHas('TencentCaptcha') || htmlHas('turing.captcha')) {
335
- return createResult(true, 'qcloud-captcha')
369
+ if (hasAnyHtml(['TencentCaptcha', 'turing.captcha'])) {
370
+ return byHtml('qcloud-captcha')
336
371
  }
337
372
 
338
373
  // AliExpress CAPTCHA: Check for punish?x5secdata in URL
339
374
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-aliexpress.json
340
- if (urlHas('punish\\?x5secdata', true)) {
341
- return createResult(true, 'aliexpress-captcha')
375
+ if (hasAnyUrl([/punish\?x5secdata/i])) {
376
+ return byUrl('aliexpress-captcha')
342
377
  }
343
378
 
344
379
  // AliExpress CAPTCHA: Check for x5secdata in html
345
- if (htmlHas('x5secdata')) {
346
- return createResult(true, 'aliexpress-captcha')
380
+ if (hasAnyHtml(['x5secdata'])) {
381
+ return byHtml('aliexpress-captcha')
347
382
  }
348
383
 
349
384
  // LinkedIn: trkCode=bf cookie ("bot filter") is set when LinkedIn blocks a request
350
- if (hasCookie('trkCode=bf')) {
351
- return createResult(true, 'linkedin')
385
+ if (hasAnyCookie(['trkCode=bf'])) {
386
+ return byCookies('linkedin')
352
387
  }
353
388
 
354
389
  // YouTube: empty title pattern indicates a degraded response requiring BotGuard JS attestation
355
390
  // Normal pages have `<title>Video Title - YouTube</title>`, bots get `<title> - YouTube</title>`
356
- if (htmlHas('<title>\\s*-\\s*YouTube<\\/title>', true)) {
357
- return createResult(true, 'youtube')
391
+ if (hasAnyHtml([/<title>\s*-\s*YouTube<\/title>/i])) {
392
+ return byHtml('youtube')
358
393
  }
359
394
 
360
395
  // AWS WAF: Check for x-amzn-waf-action or x-amzn-requestid headers
361
396
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-aws-waf.json
362
- if (getHeader('x-amzn-waf-action') || getHeader('x-amzn-requestid')) {
363
- return createResult(true, 'aws-waf')
397
+ if (hasAnyHeader(['x-amzn-waf-action', 'x-amzn-requestid'])) {
398
+ return byHeaders('aws-waf')
364
399
  }
365
400
 
366
401
  // AWS WAF: Check for aws-waf or awswaf text in html
367
- if (htmlHas('aws-waf') || htmlHas('awswaf')) {
368
- return createResult(true, 'aws-waf')
402
+ if (hasAnyHtml(['aws-waf', 'awswaf'])) {
403
+ return byHtml('aws-waf')
369
404
  }
370
405
 
371
406
  // AWS WAF: aws-waf-token cookie
372
- if (hasCookie('aws-waf-token=')) {
373
- return createResult(true, 'aws-waf')
407
+ if (hasAnyCookie(['aws-waf-token='])) {
408
+ return byCookies('aws-waf')
374
409
  }
375
410
 
376
- return createResult(false, null)
411
+ return createResult(false, null, null)
377
412
  }
378
413
 
379
414
  const isAntibot = (input = {}) => {