is-antibot 1.3.6 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/src/index.js +160 -126
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.6",
5
+ "version": "1.4.1",
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,351 +51,370 @@ 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
- // DataDome: Check for x-datadome or x-datadome-cid header presence
91
- // 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')
128
+ // DataDome: x-datadome header presence.
129
+ // Note: `x-datadome: protected` can appear on successful responses.
130
+ const xDatadome = getHeader('x-datadome')
131
+ if (xDatadome && String(xDatadome).toLowerCase() !== 'protected') {
132
+ return byHeaders('datadome')
133
+ }
134
+
135
+ // DataDome: x-datadome-cid header presence
136
+ if (hasAnyHeader(['x-datadome-cid'])) {
137
+ return byHeaders('datadome')
94
138
  }
95
139
 
96
140
  // DataDome: datadome tracking cookie
97
- if (hasCookie('datadome=')) {
98
- return createResult(true, 'datadome')
141
+ if (hasAnyCookie(['datadome='])) {
142
+ return byCookies('datadome')
99
143
  }
100
144
 
101
145
  // PerimeterX: Check for X-PX-Authorization header (primary indicator)
102
146
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-perimeterx.json
103
147
  if (getHeader('x-px-authorization')) {
104
- return createResult(true, 'perimeterx')
148
+ return byHeaders('perimeterx')
105
149
  }
106
150
 
107
151
  // PerimeterX: Check for window._pxAppId, pxInit, or _pxAction in html
108
152
  // 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')
153
+ if (hasAnyHtml(['window._pxAppId', 'pxInit', '_pxAction'])) {
154
+ return byHtml('perimeterx')
111
155
  }
112
156
 
113
157
  // PerimeterX: _px3 or _pxhd cookies
114
- if (hasCookie('_px3=') || hasCookie('_pxhd=')) {
115
- return createResult(true, 'perimeterx')
158
+ if (hasAnyCookie(['_px3=', '_pxhd='])) {
159
+ return byCookies('perimeterx')
116
160
  }
117
161
 
118
162
  // Shape Security: Check for dynamic header patterns x-[8chars]-[abcdfz]
119
163
  // These headers use 8 random characters followed by suffixes like -a, -b, -c, -d, -f, or -z
120
164
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
121
- const headerNames = Object.keys(headers)
165
+ const headerNames = getHeaderNames(headers)
122
166
  for (const name of headerNames) {
123
167
  if (/^x-[a-z0-9]{8}-[abcdfz]$/i.test(name)) {
124
- return createResult(true, 'shapesecurity')
168
+ return byHeaders('shapesecurity')
125
169
  }
126
170
  }
127
171
 
128
172
  // Shape Security: Check for 'shapesecurity' text in response html
129
173
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
130
- if (htmlHas('shapesecurity')) {
131
- return createResult(true, 'shapesecurity')
174
+ if (hasAnyHtml(['shapesecurity'])) {
175
+ return byHtml('shapesecurity')
132
176
  }
133
177
 
134
178
  // Kasada: Check for x-kasada or x-kasada-challenge headers
135
179
  // 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')
180
+ if (hasAnyHeader(['x-kasada', 'x-kasada-challenge'])) {
181
+ return byHeaders('kasada')
138
182
  }
139
183
 
140
184
  // Kasada: Check for __kasada global object or kasada.js script in html
141
185
  // 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')
186
+ if (hasAnyHtml(['__kasada', 'kasada.js'])) {
187
+ return byHtml('kasada')
144
188
  }
145
189
 
146
190
  // Imperva/Incapsula: Check for x-cdn header with 'Incapsula' value or x-iinfo header
147
191
  // 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')
192
+ if (getHeader('x-cdn') === 'Incapsula' || hasAnyHeader(['x-iinfo'])) {
193
+ return byHeaders('imperva')
150
194
  }
151
195
 
152
196
  // Imperva/Incapsula: Check for 'incapsula' or 'imperva' text in response html
153
197
  // 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')
198
+ if (hasAnyHtml(['incapsula', 'imperva'])) {
199
+ return byHtml('imperva')
156
200
  }
157
201
 
158
202
  // Imperva/Incapsula: incap_ses_, visid_incap_, or reese84 cookies
159
203
  // 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')
204
+ if (hasAnyCookie(['incap_ses_', 'visid_incap_', 'reese84='])) {
205
+ return byCookies('imperva')
166
206
  }
167
207
 
168
208
  // Reblaze: rbzid or rbzsessionid cookies
169
209
  // 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')
210
+ if (hasAnyCookie(['rbzid=', 'rbzsessionid='])) {
211
+ return byCookies('reblaze')
172
212
  }
173
213
 
174
214
  // Reblaze: Check for 'reblaze' text in response html
175
- if (htmlHas('reblaze')) {
176
- return createResult(true, 'reblaze')
215
+ if (hasAnyHtml(['reblaze'])) {
216
+ return byHtml('reblaze')
177
217
  }
178
218
 
179
219
  // Cheq: Check for CheqSdk or cheqzone.com in html
180
220
  // 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')
221
+ if (hasAnyHtml(['CheqSdk', 'cheqzone.com'])) {
222
+ return byHtml('cheq')
183
223
  }
184
224
 
185
225
  // 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')
226
+ if (hasAnyUrl([/cheqzone\.com/i, /cheq\.ai/i])) {
227
+ return byUrl('cheq')
188
228
  }
189
229
 
190
230
  // Sucuri: Check for 'sucuri' text in response html
191
231
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-sucuri.json
192
- if (htmlHas('sucuri')) {
193
- return createResult(true, 'sucuri')
232
+ if (hasAnyHtml(['sucuri'])) {
233
+ return byHtml('sucuri')
194
234
  }
195
235
 
196
236
  // ThreatMetrix: Check for 'ThreatMetrix' in html
197
237
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-threatmetrix.json
198
- if (htmlHas('ThreatMetrix')) {
199
- return createResult(true, 'threatmetrix')
238
+ if (hasAnyHtml(['ThreatMetrix'])) {
239
+ return byHtml('threatmetrix')
200
240
  }
201
241
 
202
242
  // ThreatMetrix: Check for fp/check.js fingerprint endpoint in URL
203
- if (urlHas('fp/check.js')) {
204
- return createResult(true, 'threatmetrix')
243
+ if (hasAnyUrl(['fp/check.js'])) {
244
+ return byUrl('threatmetrix')
205
245
  }
206
246
 
207
247
  // Meetrics: Check for 'meetrics' text in response html
208
248
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-meetrics.json
209
- if (htmlHas('meetrics')) {
210
- return createResult(true, 'meetrics')
249
+ if (hasAnyHtml(['meetrics'])) {
250
+ return byHtml('meetrics')
211
251
  }
212
252
 
213
253
  // Meetrics: Check for meetrics.com in URL
214
- if (urlHas('meetrics\\.com', true)) {
215
- return createResult(true, 'meetrics')
254
+ if (hasAnyUrl([/meetrics\.com/i])) {
255
+ return byUrl('meetrics')
216
256
  }
217
257
 
218
258
  // Ocule: Check for ocule.co.uk in html
219
259
  // 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')
260
+ if (hasAnyHtml(['ocule.co.uk'])) {
261
+ return byHtml('ocule')
222
262
  }
223
263
 
224
264
  // Ocule: Check for ocule.co.uk in URL
225
- if (urlHas('ocule\\.co\\.uk', true)) {
226
- return createResult(true, 'ocule')
265
+ if (hasAnyUrl([/ocule\.co\.uk/i])) {
266
+ return byUrl('ocule')
227
267
  }
228
268
 
229
269
  // reCAPTCHA: Check for recaptcha/api, google.com/recaptcha, gstatic.com/recaptcha, or recaptcha.net in URL
230
270
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
231
271
  if (
232
- urlHas('recaptcha/api') ||
233
- urlHas('google\\.com/recaptcha', true) ||
234
- urlHas('gstatic.com/recaptcha') ||
235
- urlHas('recaptcha.net')
272
+ hasAnyUrl(['recaptcha/api', 'gstatic.com/recaptcha', 'recaptcha.net']) ||
273
+ hasAnyUrl([/google\.com\/recaptcha/i])
236
274
  ) {
237
- return createResult(true, 'recaptcha')
275
+ return byUrl('recaptcha')
238
276
  }
239
277
 
240
278
  // reCAPTCHA: Check for grecaptcha API usage in html (JavaScript indicator)
241
279
  // Note: plain "grecaptcha" is too broad (e.g. ".grecaptcha-badge" CSS appears on normal YouTube pages)
242
280
  if (
243
- htmlHas(
244
- '\\b(?:window\\.)?grecaptcha\\s*\\.(?:execute|render|ready|getResponse|enterprise)\\b',
245
- true
246
- ) ||
247
- htmlHas('\\b(?:window\\.)?grecaptcha\\s*\\(', true) ||
248
- htmlHas('\\b__grecaptcha_cfg\\b', true)
281
+ hasAnyHtml([
282
+ /\b(?:window\.)?grecaptcha\s*\.(?:execute|render|ready|getResponse|enterprise)\b/i,
283
+ /\b(?:window\.)?grecaptcha\s*\(/i,
284
+ /\b__grecaptcha_cfg\b/i
285
+ ])
249
286
  ) {
250
- return createResult(true, 'recaptcha')
287
+ return byHtml('recaptcha')
251
288
  }
252
289
 
253
290
  // reCAPTCHA: Check for g-recaptcha container class in html
254
291
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
255
- if (htmlHas('g-recaptcha')) {
256
- return createResult(true, 'recaptcha')
292
+ if (hasAnyHtml(['g-recaptcha'])) {
293
+ return byHtml('recaptcha')
257
294
  }
258
295
 
259
296
  // hCaptcha: Check for hcaptcha.com domain in URL
260
297
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
261
- if (urlHas('hcaptcha\\.com', true)) {
262
- return createResult(true, 'hcaptcha')
298
+ if (hasAnyUrl([/hcaptcha\.com/i])) {
299
+ return byUrl('hcaptcha')
263
300
  }
264
301
 
265
302
  // hCaptcha: Check for hcaptcha.com API domain or h-captcha container class in html
266
303
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
267
304
  // Note: bare 'hcaptcha' matches too broadly (could appear in articles discussing hCaptcha)
268
- if (htmlHas('hcaptcha.com') || htmlHas('h-captcha')) {
269
- return createResult(true, 'hcaptcha')
305
+ if (hasAnyHtml(['hcaptcha.com', 'h-captcha'])) {
306
+ return byHtml('hcaptcha')
270
307
  }
271
308
 
272
309
  // FunCaptcha (Arkose Labs): Check for arkoselabs.com or funcaptcha in URL
273
310
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
274
- if (urlHas('arkoselabs\\.com', true) || urlHas('funcaptcha')) {
275
- return createResult(true, 'funcaptcha')
311
+ if (hasAnyUrl([/arkoselabs\.com/i]) || hasAnyUrl(['funcaptcha'])) {
312
+ return byUrl('funcaptcha')
276
313
  }
277
314
 
278
315
  // FunCaptcha (Arkose Labs): Check for arkoselabs.com API domain or funcaptcha in html
279
316
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
280
317
  // Note: bare 'arkose' matches too broadly (e.g. Facebook bundles Arkose SDK for login without blocking content)
281
- if (htmlHas('arkoselabs.com') || htmlHas('funcaptcha')) {
282
- return createResult(true, 'funcaptcha')
318
+ if (hasAnyHtml(['arkoselabs.com', 'funcaptcha'])) {
319
+ return byHtml('funcaptcha')
283
320
  }
284
321
 
285
322
  // GeeTest: Check for geetest.com domain in URL
286
323
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
287
- if (urlHas('geetest\\.com', true)) {
288
- return createResult(true, 'geetest')
324
+ if (hasAnyUrl([/geetest\.com/i])) {
325
+ return byUrl('geetest')
289
326
  }
290
327
 
291
328
  // GeeTest: Check for geetest object or text in html
292
329
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
293
330
  // Note: bare 'gt.js' removed (too generic, any script named gt.js would match)
294
- if (htmlHas('geetest')) {
295
- return createResult(true, 'geetest')
331
+ if (hasAnyHtml(['geetest'])) {
332
+ return byHtml('geetest')
296
333
  }
297
334
 
298
335
  // Cloudflare Turnstile: Check for challenges.cloudflare.com/turnstile in URL
299
- if (urlHas('challenges\\.cloudflare\\.com/turnstile', true)) {
300
- return createResult(true, 'cloudflare-turnstile')
336
+ if (hasAnyUrl([/challenges\.cloudflare\.com\/turnstile/i])) {
337
+ return byUrl('cloudflare-turnstile')
301
338
  }
302
339
 
303
340
  // Cloudflare Turnstile: Check for cf-turnstile class or turnstile API script in html
304
341
  // Note: bare 'turnstile' matches too broadly (common English word)
305
- if (
306
- htmlHas('cf-turnstile') ||
307
- htmlHas('challenges.cloudflare.com/turnstile')
308
- ) {
309
- return createResult(true, 'cloudflare-turnstile')
342
+ if (hasAnyHtml(['cf-turnstile', 'challenges.cloudflare.com/turnstile'])) {
343
+ return byHtml('cloudflare-turnstile')
310
344
  }
311
345
 
312
346
  // Friendly Captcha: Check for friendlycaptcha.com in URL
313
347
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-friendlycaptcha.json
314
- if (urlHas('friendlycaptcha\\.com', true)) {
315
- return createResult(true, 'friendly-captcha')
348
+ if (hasAnyUrl([/friendlycaptcha\.com/i])) {
349
+ return byUrl('friendly-captcha')
316
350
  }
317
351
 
318
352
  // Friendly Captcha: Check for frc-captcha container or friendlyChallenge object in html
319
- if (htmlHas('frc-captcha') || htmlHas('friendlyChallenge')) {
320
- return createResult(true, 'friendly-captcha')
353
+ if (hasAnyHtml(['frc-captcha', 'friendlyChallenge'])) {
354
+ return byHtml('friendly-captcha')
321
355
  }
322
356
 
323
357
  // Captcha.eu: Check for captcha.eu in URL
324
358
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-captchaeu.json
325
- if (urlHas('captcha\\.eu', true)) {
326
- return createResult(true, 'captcha-eu')
359
+ if (hasAnyUrl([/captcha\.eu/i])) {
360
+ return byUrl('captcha-eu')
327
361
  }
328
362
 
329
363
  // Captcha.eu: Check for CaptchaEU or captchaeu in html
330
- if (htmlHas('CaptchaEU') || htmlHas('captchaeu')) {
331
- return createResult(true, 'captcha-eu')
364
+ if (hasAnyHtml(['CaptchaEU', 'captchaeu'])) {
365
+ return byHtml('captcha-eu')
332
366
  }
333
367
 
334
368
  // QCloud Captcha (Tencent): Check for turing.captcha.qcloud.com in URL
335
369
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-qcloud.json
336
- if (urlHas('turing\\.captcha\\.qcloud\\.com', true)) {
337
- return createResult(true, 'qcloud-captcha')
370
+ if (hasAnyUrl([/turing\.captcha\.qcloud\.com/i])) {
371
+ return byUrl('qcloud-captcha')
338
372
  }
339
373
 
340
374
  // QCloud Captcha: Check for TencentCaptcha or turing.captcha in html
341
- if (htmlHas('TencentCaptcha') || htmlHas('turing.captcha')) {
342
- return createResult(true, 'qcloud-captcha')
375
+ if (hasAnyHtml(['TencentCaptcha', 'turing.captcha'])) {
376
+ return byHtml('qcloud-captcha')
343
377
  }
344
378
 
345
379
  // AliExpress CAPTCHA: Check for punish?x5secdata in URL
346
380
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-aliexpress.json
347
- if (urlHas('punish\\?x5secdata', true)) {
348
- return createResult(true, 'aliexpress-captcha')
381
+ if (hasAnyUrl([/punish\?x5secdata/i])) {
382
+ return byUrl('aliexpress-captcha')
349
383
  }
350
384
 
351
385
  // AliExpress CAPTCHA: Check for x5secdata in html
352
- if (htmlHas('x5secdata')) {
353
- return createResult(true, 'aliexpress-captcha')
386
+ if (hasAnyHtml(['x5secdata'])) {
387
+ return byHtml('aliexpress-captcha')
354
388
  }
355
389
 
356
390
  // LinkedIn: trkCode=bf cookie ("bot filter") is set when LinkedIn blocks a request
357
- if (hasCookie('trkCode=bf')) {
358
- return createResult(true, 'linkedin')
391
+ if (hasAnyCookie(['trkCode=bf'])) {
392
+ return byCookies('linkedin')
359
393
  }
360
394
 
361
395
  // YouTube: empty title pattern indicates a degraded response requiring BotGuard JS attestation
362
396
  // Normal pages have `<title>Video Title - YouTube</title>`, bots get `<title> - YouTube</title>`
363
- if (htmlHas('<title>\\s*-\\s*YouTube<\\/title>', true)) {
364
- return createResult(true, 'youtube')
397
+ if (hasAnyHtml([/<title>\s*-\s*YouTube<\/title>/i])) {
398
+ return byHtml('youtube')
365
399
  }
366
400
 
367
401
  // AWS WAF: Check for x-amzn-waf-action or x-amzn-requestid headers
368
402
  // Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-aws-waf.json
369
- if (getHeader('x-amzn-waf-action') || getHeader('x-amzn-requestid')) {
370
- return createResult(true, 'aws-waf')
403
+ if (hasAnyHeader(['x-amzn-waf-action', 'x-amzn-requestid'])) {
404
+ return byHeaders('aws-waf')
371
405
  }
372
406
 
373
407
  // AWS WAF: Check for aws-waf or awswaf text in html
374
- if (htmlHas('aws-waf') || htmlHas('awswaf')) {
375
- return createResult(true, 'aws-waf')
408
+ if (hasAnyHtml(['aws-waf', 'awswaf'])) {
409
+ return byHtml('aws-waf')
376
410
  }
377
411
 
378
412
  // AWS WAF: aws-waf-token cookie
379
- if (hasCookie('aws-waf-token=')) {
380
- return createResult(true, 'aws-waf')
413
+ if (hasAnyCookie(['aws-waf-token='])) {
414
+ return byCookies('aws-waf')
381
415
  }
382
416
 
383
- return createResult(false, null)
417
+ return createResult(false, null, null)
384
418
  }
385
419
 
386
420
  const isAntibot = (input = {}) => {