capskip 1.1.0 → 1.3.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.
@@ -23,8 +23,14 @@ method returns a `Promise`.
23
23
  | reCAPTCHA v3 | `recaptcha(sitekey, url, { version: 'v3' })` | `userrecaptcha` + `version=v3` |
24
24
  | Cloudflare Turnstile | `turnstile()` | `turnstile` |
25
25
  | GeeTest v3 (slide) | `geetest()` | `geetest` |
26
+ | ALTCHA (proof-of-work) | `altcha()` | `altcha` |
27
+ | Capy Puzzle (slide) | `capy()` | `capy` |
28
+ | CaptchaFox (widget) | `captchafox()` | `captchafox` |
29
+ | Friendly Captcha (proof-of-work) | `friendlyCaptcha()` | `friendly_captcha` |
26
30
 
27
- **Proxy** is supported for reCAPTCHA, Turnstile, and GeeTest — not for image captcha.
31
+ **Proxy** is supported for every method except image captcha. For ALTCHA the
32
+ proxy is used only for the `challengeUrl` fetch; for Capy, only for the
33
+ puzzle-image fetch, which is the only request a Capy solve makes.
28
34
 
29
35
  ---
30
36
 
@@ -267,6 +273,345 @@ rather than `defaultTimeout`.
267
273
 
268
274
  ---
269
275
 
276
+ ## 6. ALTCHA — `altcha(url, { ... })`
277
+
278
+ ALTCHA is not a recognition captcha. There is no image, audio or text to read:
279
+ the site issues a proof-of-work challenge and the client must brute-force a
280
+ number that satisfies it. A solve is therefore deterministic and cheap —
281
+ typically milliseconds.
282
+
283
+ ### POST `/in.php`
284
+
285
+ | Parameter | Type | Required | Description |
286
+ |---|---|---|---|
287
+ | `key` | string | Yes | CapSkip API key |
288
+ | `method` | string | Yes | `altcha` |
289
+ | `pageurl` | string | Yes | Full URL of the page the challenge came from |
290
+ | `challenge_url` | string | One of the two | Endpoint CapSkip fetches the challenge from |
291
+ | `challenge_json` | string | One of the two | The challenge document itself, as a JSON string |
292
+ | `json` | int | No | `0` plain text, `1` JSON |
293
+ | `proxy` | string | No | Proxy address — used **only** for the `challenge_url` fetch |
294
+ | `proxytype` | string | No | Proxy type |
295
+
296
+ Sending both challenge parameters is allowed: the inline `challengeJson` wins,
297
+ because fetching would only re-obtain what you already supplied.
298
+
299
+ ### Getting the challenge
300
+
301
+ Open DevTools → Network on the target page and look for the request the
302
+ `<altcha-widget>` makes for its challenge (often something like
303
+ `/altcha/challenge`). The request URL is your `challengeUrl`; its JSON response
304
+ is your `challengeJson`.
305
+
306
+ The widget attribute that names that endpoint depends on the widget version, so
307
+ read the page source rather than assuming:
308
+
309
+ | Widget | Attribute |
310
+ |---|---|
311
+ | v1 / v2 | `challengeurl="…"`, with a separate `challengejson="…"` for an inline challenge |
312
+ | v3+ | `challenge="…"` — the same attribute takes either a URL or the challenge data |
313
+
314
+ > **Challenges expire, and the window is short** — some sites inside two minutes.
315
+ > Once expired, the site refuses the solution with a bare "verification failed"
316
+ > that looks exactly like a wrong answer. Fetch the challenge immediately before
317
+ > solving and submit the token promptly; do not fetch a batch in advance, and do
318
+ > not hold a token while a user fills in a form.
319
+ >
320
+ > CapSkip refuses an already-expired inline challenge immediately rather than
321
+ > burning CPU on a token that cannot work. If you passed `challengeUrl` and the
322
+ > challenge expired while the job queued, it fetches a fresh one automatically.
323
+
324
+ ### SDK usage
325
+
326
+ ```js
327
+ // CapSkip fetches the challenge for you
328
+ const result = await solver.altcha('https://example.com/signup', {
329
+ challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
330
+ });
331
+
332
+ // …or hand it the document you already have. No network request at all.
333
+ const inline = await solver.altcha('https://example.com/signup', {
334
+ challengeJson: {
335
+ algorithm: 'SHA-256', challenge: '…', salt: '…',
336
+ signature: '…', maxnumber: 1000000,
337
+ },
338
+ });
339
+
340
+ result.token; // the base64 payload to post back
341
+ result.number; // the counter that solved it
342
+ result.code; // the same string as token
343
+ ```
344
+
345
+ `challengeJson` accepts an object (serialized for you) or a JSON string.
346
+
347
+ `number` is reported for both ALTCHA generations. Their tokens differ — a legacy
348
+ payload carries it as a top-level `number`, while a proof-of-work v2 payload has
349
+ none, its counter sitting at `solution.counter` — so it is read from the server's
350
+ own `solution` object, which reports both the same way.
351
+
352
+ Post the token back in the form field the widget uses, named `altcha`:
353
+
354
+ ```js
355
+ await fetch(SIGNUP_URL, {
356
+ method: 'POST',
357
+ body: new URLSearchParams({
358
+ email: 'someone@example.com',
359
+ altcha: result.token,
360
+ }),
361
+ });
362
+ ```
363
+
364
+ Do not re-encode, trim or re-order the token: it is base64 of a JSON document
365
+ whose fields are covered by the server's HMAC signature, so any modification
366
+ invalidates it. Some integrations read the payload from a JSON body field
367
+ instead — check what the page's own submit sends and mirror it.
368
+
369
+ Unlike GeeTest and reCAPTCHA this is CPU proof-of-work rather than a browser
370
+ solve, so it uses `defaultTimeout`, not `recaptchaTimeout`.
371
+
372
+ ### Algorithms
373
+
374
+ CapSkip supports the legacy scheme (SHA-1/256/384/512) and PoW v2 with PBKDF2 or
375
+ SHA. **Argon2id and scrypt are refused**, not attempted: a task using one returns
376
+ `ERROR_CAPTCHA_UNSOLVABLE` and is never retried. ALTCHA itself recommends PBKDF2
377
+ as the default, so this affects a minority of sites.
378
+
379
+ All three widget types (`native`, `checkbox`, `switch`) work — the distinction is
380
+ purely visual and never reaches CapSkip.
381
+
382
+ ---
383
+
384
+ ## 7. Capy Puzzle — `capy(sitekey, url, { ... })`
385
+
386
+ Capy is a slide puzzle: a piece has to be dragged into the hole cut out of a
387
+ photograph. CapSkip locates the hole and produces the drag path — one HTTP fetch
388
+ plus pixel math, no browser.
389
+
390
+ ### POST `/in.php`
391
+
392
+ | Parameter | Type | Required | Description |
393
+ |---|---|---|---|
394
+ | `key` | string | Yes | CapSkip API key |
395
+ | `method` | string | Yes | `capy` |
396
+ | `captchakey` | string | Yes | The site's public Capy key, conventionally prefixed `PUZZLE_`. The SDK sends its `sitekey` argument here. |
397
+ | `pageurl` | string | Yes | Full URL of the page the captcha is on |
398
+ | `api_server` | string | No | Root of the Capy API the key lives behind. Default `https://jp.api.capy.me` |
399
+ | `version` | string | No | `puzzle` (the default). Only the puzzle family is solved |
400
+ | `useragent` | string | No | User-Agent to send with the puzzle request |
401
+ | `json` | int | No | `0` plain text, `1` JSON |
402
+ | `proxy` | string | No | Proxy address |
403
+ | `proxytype` | string | No | Proxy type |
404
+
405
+ `sitekey` and `websiteKey` are accepted by the server as aliases for
406
+ `captchakey`; the SDK sends the documented name.
407
+
408
+ `version: 'avatar'` is refused by the SDK before the request is made. Avatar is a
409
+ different challenge behind a different endpoint, and answering it with a puzzle
410
+ answer would bill for a solve the target site rejects, indistinguishably from a
411
+ broken solver.
412
+
413
+ > `api.capy.me` no longer resolves, although several solver services still
414
+ > document it. The live host is `jp.api.capy.me`; if a site's widget points
415
+ > somewhere else, pass that as `api_server`.
416
+
417
+ ### Getting the captcha key
418
+
419
+ It is in the page source as `capy_captchakey`, or in the widget script URL:
420
+ `<script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX">`.
421
+
422
+ ### GET `/res.php`
423
+
424
+ The answer is **not a token**. It is an object, which the SDK expands into three
425
+ result fields. See [Return value](#return-value).
426
+
427
+ ### SDK usage
428
+
429
+ ```js
430
+ const result = await solver.capy(
431
+ 'PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v',
432
+ 'https://example.com/login',
433
+ );
434
+
435
+ // With a non-default API server
436
+ const result = await solver.capy(
437
+ 'PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v',
438
+ 'https://example.com/login',
439
+ { api_server: 'https://jp.api.capy.me/' },
440
+ );
441
+
442
+ // capy_captchakey, capy_challengekey, capy_answer
443
+ result.captchakey, result.challengekey, result.answer;
444
+ ```
445
+
446
+ ### Timing
447
+
448
+ Capy grades the wall-clock gap between issuing the puzzle and verifying the
449
+ answer, and refuses anything superhuman with `CAPTCHA verification failed` — the
450
+ same message a wrong answer gets. CapSkip therefore holds every result until two
451
+ seconds have elapsed since it drew the puzzle. This is invisible if you poll: the
452
+ task simply takes about two seconds instead of a fifth of one. Nothing to
453
+ configure.
454
+
455
+ The challenge key is single-use and short-lived, so submit the three values
456
+ promptly rather than caching them.
457
+
458
+ ---
459
+
460
+ ## 8. CaptchaFox — `captchafox(sitekey, url, { ... })`
461
+
462
+ CaptchaFox scores the browser itself rather than asking the visitor to read
463
+ anything; most visitors never see a puzzle. CapSkip drives the real widget in a
464
+ real browser and returns the verification token.
465
+
466
+ ### POST `/in.php`
467
+
468
+ | Parameter | Type | Required | Description |
469
+ |---|---|---|---|
470
+ | `key` | string | Yes | CapSkip API key |
471
+ | `method` | string | Yes | `captchafox` |
472
+ | `sitekey` | string | Yes | The public key the widget renders with, conventionally prefixed `sk_` |
473
+ | `pageurl` | string | Yes | Full URL of the page the widget appears on |
474
+ | `api_server` | string | No | Widget entry point. Default `https://cdn.captchafox.com/` |
475
+ | `useragent` | string | No | Accepted for compatibility and not applied — CapSkip uses its own browser's identity |
476
+ | `json` | int | No | `0` plain text, `1` JSON |
477
+ | `proxy` | string | No | Proxy address |
478
+ | `proxytype` | string | No | Proxy type |
479
+
480
+ ### Choosing the widget source
481
+
482
+ | `api_server` | Token | Default |
483
+ |---|---|---|
484
+ | `https://cdn.captchafox.com/` | Plain | Yes |
485
+ | `https://s.uicdn.com/mampkg/…` | `MAM_` prefixed | No |
486
+
487
+ Read the value from the `<script>` tag that loads the widget. If you send the
488
+ wrong one the solve still succeeds, but the token comes back in a format the
489
+ target site will not accept — a silent verification failure rather than an error.
490
+
491
+ ### The page URL has to match the key
492
+
493
+ CaptchaFox keys are registered against a list of allowed domains and the service
494
+ checks the host before issuing anything. A correct key used on a page outside
495
+ that list is refused permanently, not intermittently. CapSkip reports that case
496
+ rather than retrying it, because retrying cannot help.
497
+
498
+ ### Challenge types
499
+
500
+ | Challenge | Frequency | Supported |
501
+ |---|---|---|
502
+ | Invisible | Usually | Yes |
503
+ | Slide | Sometimes | Yes |
504
+ | Image select | Rarely | No |
505
+ | Audio | Rarely | No |
506
+
507
+ The two unsupported ones are uncommon and a retry usually draws a different
508
+ challenge, so treat an unsolvable result as a signal to resubmit rather than a
509
+ permanent failure of the key.
510
+
511
+ ### SDK usage
512
+
513
+ ```js
514
+ const result = await solver.captchafox(
515
+ 'sk_xtNxpk6fCdFbxh1_xJeGflSdCE9tn99G',
516
+ 'https://example.com/signup',
517
+ );
518
+
519
+ // Post in the form field named cf-captcha-response
520
+ result.token;
521
+
522
+ // Submit under the UA that minted the token, not your own
523
+ result.userAgent;
524
+ ```
525
+
526
+ Uses `recaptchaTimeout` — it is a real browser session, and longer again when an
527
+ interactive challenge is drawn.
528
+
529
+ ---
530
+
531
+ ## 9. Friendly Captcha — `friendlyCaptcha(sitekey, url, { ... })`
532
+
533
+ Proof-of-work, with nothing shown on screen. The widget searches for values that
534
+ hash below a difficulty the service sets per request.
535
+
536
+ ### POST `/in.php`
537
+
538
+ | Parameter | Type | Required | Description |
539
+ |---|---|---|---|
540
+ | `key` | string | Yes | CapSkip API key |
541
+ | `method` | string | Yes | `friendly_captcha` |
542
+ | `sitekey` | string | Yes | The `data-sitekey` of the element carrying `class="frc-captcha"` |
543
+ | `pageurl` | string | Yes | Full URL of the page the widget appears on |
544
+ | `version` | string | No | `v1` (default) or `v2`; a bare `1` or `2` is accepted |
545
+ | `module_script` | string | No | `src` of the widget script tag carrying `type="module"` |
546
+ | `nomodule_script` | string | No | `src` of the widget script tag carrying `nomodule` |
547
+ | `api_server` | string | No | Data residency endpoint: `global` (default), `eu`, or a full URL |
548
+ | `useragent` | string | No | User-Agent to send with the request |
549
+ | `json` | int | No | `0` plain text, `1` JSON |
550
+ | `proxy` | string | No | Proxy address |
551
+ | `proxytype` | string | No | Proxy type |
552
+
553
+ ### Version 1 and version 2
554
+
555
+ Two entirely different protocols ship under this one name, and a sitekey does not
556
+ tell you which one a site uses — they share a brand and a sitekey namespace and
557
+ nothing else. Both are live. Solve the wrong one and you get a well-formed token
558
+ the target site rejects, with no indication that the version was the problem.
559
+
560
+ | `version` | Widget package | Script |
561
+ |---|---|---|
562
+ | `v1` | `friendly-challenge` | `widget.module.min.js` / `widget.min.js` |
563
+ | `v2` | `@friendlycaptcha/sdk` | `site.min.js` |
564
+
565
+ CapSkip decides in this order, stopping at the first answer: the `version`
566
+ parameter; then the script URL from `moduleScript` or `nomoduleScript`, which is
567
+ the most reliable signal because it is the build the site actually loads; then
568
+ v1. The SDK refuses any other `version` value locally.
569
+
570
+ ### Submitting the token
571
+
572
+ | `version` | Form field |
573
+ |---|---|
574
+ | `v1` | `frc-captcha-solution` |
575
+ | `v2` | `frc-captcha-response` |
576
+
577
+ The field names differ, which is what catches an integration moved from one to
578
+ the other. A v1 token is four dot-separated parts and runs to a few hundred
579
+ characters; a v2 token is a single opaque string beginning `AQQA.` and is roughly
580
+ six kilobytes, so size whatever carries it accordingly.
581
+
582
+ ### SDK usage
583
+
584
+ ```js
585
+ // Say which version
586
+ const result = await solver.friendlyCaptcha(
587
+ 'FCMGEMUD2M567T8G',
588
+ 'https://example.com/signup',
589
+ { version: 'v2' },
590
+ );
591
+
592
+ // …or let the script URL decide it
593
+ const result = await solver.friendlyCaptcha(
594
+ 'FCMGEMUD2M567T8G',
595
+ 'https://example.com/signup',
596
+ { moduleScript: 'https://cdn.example.com/@friendlycaptcha/sdk@0.1.6/site.min.js' },
597
+ );
598
+
599
+ // EU data-residency tenant
600
+ const result = await solver.friendlyCaptcha(
601
+ 'FCMGEMUD2M567T8G',
602
+ 'https://example.com/signup',
603
+ { version: 'v2', api_server: 'eu' },
604
+ );
605
+
606
+ result.token;
607
+ ```
608
+
609
+ Uses `recaptchaTimeout`: the service decides how much work a request is worth at
610
+ the moment it is made, so solve time is not a constant, and v2 always solves in a
611
+ browser.
612
+
613
+ ---
614
+
270
615
  ## Return value
271
616
 
272
617
  Every solve method resolves to:
@@ -279,6 +624,45 @@ Every solve method resolves to:
279
624
  }
280
625
  ```
281
626
 
627
+ ALTCHA additionally exposes `token` (the same string as `code`, named for the
628
+ form field it goes in) and `number`, the counter that solved it:
629
+
630
+ ```js
631
+ {
632
+ captchaId: '12345',
633
+ code: 'eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…',
634
+ token: 'eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…',
635
+ number: 9661,
636
+ }
637
+ ```
638
+
639
+ CaptchaFox and Friendly Captcha expose `token`, the same string as `code`, named
640
+ for the form field it goes in. CaptchaFox adds `userAgent` when the solve
641
+ reported one — the UA the browser minted the token under, not one you sent:
642
+
643
+ ```js
644
+ {
645
+ captchaId: '12345',
646
+ code: '177f50c25b845601e5c779cdb51b040d…',
647
+ token: '177f50c25b845601e5c779cdb51b040d…',
648
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) …',
649
+ }
650
+ ```
651
+
652
+ Capy returns an object rather than a token, expanded into the three fields the
653
+ target form takes (`code` keeps the raw answer):
654
+
655
+ ```js
656
+ {
657
+ captchaId: '12345',
658
+ code: { captchakey: 'PUZZLE_…', challengekey: '…', answer: '…', respKey: '' },
659
+ captchakey: 'PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v',
660
+ challengekey: 'BalY2gJaI8uA2SGVOZhqBQ3V0CYSNNGP',
661
+ answer: '0xax8ex0xax84x0xkx7qx0x18x76x…',
662
+ respKey: '',
663
+ }
664
+ ```
665
+
282
666
  GeeTest additionally expands its answer into `challenge`, `validate`, and
283
667
  `seccode` (`code` keeps the raw JSON string):
284
668
 
@@ -307,6 +691,12 @@ Convenience aliases mapped before sending to CapSkip:
307
691
  | `data_s` | `data-s` |
308
692
  | `apiServer` | `api_server` |
309
693
  | `api_subdomain` | `api_server` |
694
+ | `challengeUrl` / `challengeURL` | `challenge_url` |
695
+ | `challengeJson` / `challengeJSON` | `challenge_json` |
696
+ | `userAgent` / `user_agent` | `useragent` |
697
+ | `captchaKey` | `captchakey` |
698
+ | `moduleScript` | `module_script` |
699
+ | `nomoduleScript` / `noModuleScript` | `nomodule_script` |
310
700
  | `proxy` object | `proxy` + `proxytype` strings |
311
701
 
312
702
  ```js
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Solve an ALTCHA proof-of-work challenge.
5
+ *
6
+ * ALTCHA is not a recognition captcha -- there is nothing to read. The site
7
+ * issues a challenge and the browser must brute-force a number that satisfies
8
+ * it. CapSkip does that work for you, in milliseconds.
9
+ *
10
+ * You need the challenge, in one of two forms:
11
+ *
12
+ * * `challengeUrl` - the endpoint that serves it; CapSkip fetches it for you
13
+ * * `challengeJson` - the challenge document itself, if you already have it
14
+ *
15
+ * To find them, open DevTools -> Network on the target page and look for the
16
+ * request the `<altcha-widget>` makes for its challenge (often something like
17
+ * `/altcha/challenge`). The request URL is your `challengeUrl`; its JSON
18
+ * response is your `challengeJson`.
19
+ *
20
+ * Note the widget attribute that names the endpoint changed between versions:
21
+ * v1/v2 use `challengeurl="..."`, while v3+ uses `challenge="..."` for both a
22
+ * URL and inline data. Read the page source rather than assuming.
23
+ *
24
+ * Challenges expire fast -- some sites inside two minutes -- so fetch one
25
+ * immediately before solving and post the token promptly. An expired challenge
26
+ * is rejected with a bare "verification failed" that looks exactly like a wrong
27
+ * answer.
28
+ *
29
+ * This example issues its own challenge the way a site's server would, so it
30
+ * runs as-is with no third-party dependency. Swap in your target's endpoint to
31
+ * use it for real.
32
+ */
33
+
34
+ const crypto = require('crypto');
35
+
36
+ const { CapSkip } = require('../src');
37
+
38
+ const solver = new CapSkip({
39
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
40
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
41
+ port: Number(process.env.CAPSKIP_PORT || 8080),
42
+ });
43
+
44
+ const PAGE_URL = 'https://example.com/signup';
45
+
46
+ /**
47
+ * Mint an ALTCHA challenge, exactly as a site's own server would.
48
+ *
49
+ * Replace this with a fetch of your target's challenge endpoint -- or skip it
50
+ * entirely and pass `challengeUrl` so CapSkip does the fetching.
51
+ */
52
+ function issueChallenge(number = 54321) {
53
+ const salt = `${crypto.randomBytes(12).toString('hex')}?expires=${
54
+ Math.floor(Date.now() / 1000) + 600}`;
55
+ return {
56
+ algorithm: 'SHA-256',
57
+ challenge: crypto.createHash('sha256').update(`${salt}${number}`).digest('hex'),
58
+ salt,
59
+ signature: '0'.repeat(64),
60
+ maxnumber: 100000,
61
+ };
62
+ }
63
+
64
+ (async () => {
65
+ // --- Option A: you already have the challenge document ---------------------
66
+ // No network request at all: CapSkip solves it locally.
67
+ const result = await solver.altcha(PAGE_URL, { challengeJson: issueChallenge() });
68
+
69
+ console.log('Captcha ID:', result.captchaId);
70
+ console.log('Number: ', result.number);
71
+ console.log('Token: ', `${result.token.slice(0, 60)}...`);
72
+
73
+ // --- Option B: let CapSkip fetch the challenge ------------------------------
74
+ // Point it at the endpoint the widget calls. Add `proxy` if the endpoint
75
+ // should be fetched from a particular IP -- the proxy is used only for that
76
+ // fetch, never for the solve itself.
77
+ //
78
+ // const result = await solver.altcha(PAGE_URL, {
79
+ // challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
80
+ // proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
81
+ // });
82
+
83
+ // Post the token back in the form field the widget uses, named `altcha`:
84
+ //
85
+ // await fetch(SIGNUP_URL, {
86
+ // method: 'POST',
87
+ // body: new URLSearchParams({
88
+ // email: 'someone@example.com',
89
+ // altcha: result.token,
90
+ // }),
91
+ // });
92
+ //
93
+ // Do not re-encode, trim or re-order it. The token is base64 of a JSON
94
+ // document whose fields are covered by the server's HMAC signature, so any
95
+ // modification invalidates it.
96
+
97
+ // `code` holds the same string as `token`, which is what you forward if you
98
+ // are porting code written against another solver's API.
99
+ console.assert(result.code === result.token);
100
+ })().catch((err) => {
101
+ console.error(err);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Solve a CaptchaFox challenge.
5
+ *
6
+ * CaptchaFox scores the browser itself rather than asking the visitor to read
7
+ * anything, so most solves draw no puzzle at all. CapSkip drives the real widget
8
+ * in a real browser and returns the verification token it would have produced.
9
+ *
10
+ * You need two values, both read off the target page:
11
+ *
12
+ * * sitekey - the public key the widget renders with, conventionally prefixed
13
+ * `sk_`. It is on the widget container as `data-sitekey`, or in the
14
+ * `captchafox.render(...)` call. If the page builds the widget at runtime,
15
+ * open DevTools -> Network and find the request to api.captchafox.com: the
16
+ * key is the path segment after /captcha/.
17
+ * * pageurl - the full URL of the page the widget appears on
18
+ *
19
+ * The page URL has to match: CaptchaFox keys are registered against a list of
20
+ * allowed domains and the service checks the host before it issues anything. A
21
+ * key that is correct but used on a page outside that list is refused
22
+ * permanently, not intermittently -- so if a sitekey fails immediately and
23
+ * consistently, check that you are sending the page the widget actually runs on
24
+ * rather than a search page, a redirect or a shortened link.
25
+ */
26
+
27
+ const { CapSkip } = require('capskip');
28
+
29
+ const solver = new CapSkip({
30
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
31
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
32
+ port: Number(process.env.CAPSKIP_PORT || 8080),
33
+ });
34
+
35
+ const SITEKEY = 'sk_xtNxpk6fCdFbxh1_xJeGflSdCE9tn99G';
36
+ const PAGE_URL = 'https://example.com/signup';
37
+
38
+ async function main() {
39
+ const result = await solver.captchafox(SITEKEY, PAGE_URL);
40
+
41
+ console.log('Captcha ID:', result.captchaId);
42
+ console.log('Token: ', `${result.token.slice(0, 60)}...`);
43
+ console.log('User-Agent:', result.userAgent || '(none reported)');
44
+
45
+ // --- Choosing the widget source ------------------------------------------
46
+ // CaptchaFox publishes its widget from two places, and which one a site loads
47
+ // decides the shape of the token it expects back. Send api_server only when
48
+ // the target page does not use the default (https://cdn.captchafox.com/).
49
+ //
50
+ // await solver.captchafox(SITEKEY, PAGE_URL, {
51
+ // api_server: 'https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/',
52
+ // });
53
+ //
54
+ // That one returns a MAM_ prefixed token. Read the value from the <script>
55
+ // tag on the target page: if you send the wrong one the solve still succeeds,
56
+ // but the token comes back in a format the site will not accept, which looks
57
+ // like a silent verification failure rather than an error.
58
+ //
59
+ // --- With a per-request proxy --------------------------------------------
60
+ // CapSkip does not force a proxy, but CaptchaFox scores the network a widget
61
+ // runs on as well as the browser, so repeated solves from one address push
62
+ // that address toward the interactive challenges and then toward refusals.
63
+ //
64
+ // await solver.captchafox(SITEKEY, PAGE_URL, {
65
+ // proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
66
+ // });
67
+
68
+ // Post the token back in the form field the widget uses, named
69
+ // `cf-captcha-response`. Treat it as opaque and pass it through unchanged --
70
+ // it is verified server side against the session that produced it, so any
71
+ // edit invalidates it.
72
+ //
73
+ // await fetch(SIGNUP_URL, {
74
+ // method: 'POST',
75
+ // headers: { 'User-Agent': result.userAgent },
76
+ // body: new URLSearchParams({
77
+ // email: 'someone@example.com',
78
+ // 'cf-captcha-response': result.token,
79
+ // }),
80
+ // });
81
+ //
82
+ // Some integrations read the token from a JSON body field instead, so check
83
+ // what the page's own submit sends and mirror it.
84
+ console.log('Form field:', { 'cf-captcha-response': `${result.token.slice(0, 40)}...` });
85
+
86
+ // Submit under the User-Agent the token was minted with, not your own.
87
+ // CapSkip solves in a real browser and uses that browser's identity, so the
88
+ // UA it reports is the one the token is bound to.
89
+ //
90
+ // Tokens are short lived: submit promptly rather than holding one while a
91
+ // user fills in a form, and solve again if the form is abandoned and resumed.
92
+
93
+ // Two of CaptchaFox's challenge types are not solvable -- image-select and
94
+ // the audio fallback, both rare. They are reported as unsolvable rather than
95
+ // waited out, so treat a failure as a signal to resubmit into a fresh
96
+ // challenge rather than as a permanent failure of the key.
97
+ }
98
+
99
+ main().catch((error) => {
100
+ console.error(`${error.name}: ${error.message}`);
101
+ process.exitCode = 1;
102
+ });