@itd-api/captcha 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @itd-api/captcha
2
2
 
3
- Получает токены собственной капчи ИТД и Cloudflare Turnstile для входа через [`itd-api`](https://github.com/KiowDev/itd-api). Активного провайдера выбирает сервер.
3
+ Получает токены собственной капчи ИТД и Cloudflare Turnstile для входа через [`itd-api`](https://github.com/KiowDev/itd-api). Сервер принимает обе: виджет выбираете вы либо он сам.
4
4
 
5
5
  [Руководство](https://kiowdev.github.io/itd-api/packages/captcha) ·
6
6
  [API из TSDoc](https://kiowdev.github.io/itd-api/api/generated/captcha/)
@@ -9,18 +9,19 @@
9
9
 
10
10
  ## Когда пакет не нужен
11
11
 
12
- Капча участвует только в самом входе по паролю ни продление сессии, ни обычные запросы,
13
- ни событийные соединения её не требуют. Пакет незачем ставить, если:
12
+ Капча участвует во входе по паролю, регистрации, сбросе пароля и подтверждении QR-входа
13
+ ни продление сессии, ни обычные запросы, ни событийные соединения её не требуют. Пакет
14
+ незачем ставить, если:
14
15
 
15
16
  - сессия уже сохранена в `FileTokenStorage` — клиент продлевает её сам;
16
17
  - токены можно [скопировать из браузера](https://kiowdev.github.io/itd-api/authentication/#токены-из-браузера), где вы уже вошли: DevTools отдают и access token, и cookie `refresh_token`;
17
18
  - access token выдаёт серверное приложение или хранилище секретов — тогда подойдёт `auth: { getToken }`;
18
- - токен капчи приходит из своего источника — `auth.captcha.getToken` принимает любую функцию.
19
+ - токен капчи приходит из своего источника — опция клиента `captcha` принимает любую функцию.
19
20
 
20
21
  ## Установка
21
22
 
22
23
  ```sh
23
- npm i @itd-api/captcha patchright
24
+ npm i @itd-api/captcha patchright@1.61.1
24
25
  npx patchright install chromium
25
26
  ```
26
27
 
@@ -38,68 +39,42 @@ import { createCaptchaSolver } from '@itd-api/captcha';
38
39
 
39
40
  const itd = new ItdClient({
40
41
  storage: new FileTokenStorage('./.itd-session.json'),
41
- auth: {
42
- email: process.env.ITD_EMAIL!,
43
- password: process.env.ITD_PASSWORD!,
44
- captcha: createCaptchaSolver(),
45
- },
42
+ auth: { email: process.env.ITD_EMAIL!, password: process.env.ITD_PASSWORD! },
43
+ captcha: createCaptchaSolver(),
46
44
  });
47
45
  ```
48
46
 
49
- `createCaptchaSolver()` отдаёт `getToken(type)`. Перед каждым входом клиент читает активного
50
- провайдера и просит решить именно его виджет, поэтому переключение провайдера сервером
51
- переживается без правок кода. Передаётся функция, а не готовый токен: токен одноразовый и
52
- живёт несколько минут. Браузер поднимается на время одного вызова и сразу закрывается.
47
+ Клиент спрашивает токен каждый раз, когда сервер требует капчу; браузер поднимается на
48
+ время одного вызова и сразу закрывается. Тот же источник, заданный контейнеру
49
+ `ItdAccounts`, достаётся каждому аккаунту.
53
50
 
54
- Решить капчу без клиента — тип называется явно:
51
+ ### Какую капчу проходить
55
52
 
56
- ```ts
57
- import { solveCaptcha, CaptchaType } from '@itd-api/captcha';
58
-
59
- const token = await solveCaptcha(CaptchaType.Itd);
60
- ```
53
+ Сервер принимает обе. Без настройки источник следует за сервером: клиент спрашивает
54
+ активного провайдера и просит пройти именно его виджет, так что переключение провайдера
55
+ переживается без правок кода.
61
56
 
62
- Провайдера с другими настройками собирает его фабрика:
57
+ Нужен конкретный виджет назовите его, и лишнего запроса к серверу не будет:
63
58
 
64
59
  ```ts
65
- import { solveCaptcha, itdCaptcha, turnstile } from '@itd-api/captcha';
60
+ import { createCaptchaSolver, CaptchaType } from '@itd-api/captcha';
66
61
 
67
- const forRegistration = await solveCaptcha(itdCaptcha({ action: 'register' }));
68
- const withOwnKey = await solveCaptcha(turnstile({ sitekey: '0x…' }), { headless: true });
62
+ createCaptchaSolver({ type: CaptchaType.Cloudflare }); // всегда Turnstile
63
+ createCaptchaSolver({ type: CaptchaType.Itd }); // всегда капча ИТД
69
64
  ```
70
65
 
71
- ## Свой провайдер
66
+ Незнакомый тип отвергается при создании, а не в момент входа.
67
+
68
+ Имя поля, в котором сервер ждёт токен, клиент при закреплённом типе берёт из своей таблицы.
69
+ Если сервер его переименовал, назовите поле сами: `createCaptchaSolver({ type, field: 'c7f2' })`.
72
70
 
73
- Новый виджет добавляется реализацией `CaptchaHandler` — менять пакет не нужно. Перехват
74
- навигации, ожидание, клик по чекбоксу и повторы одинаковы для всех виджетов и уже сделаны;
75
- описать нужно только то, чем виджет отличается:
71
+ ### Один токен без клиента
76
72
 
77
73
  ```ts
78
- import { solveCaptcha, type CaptchaHandler } from '@itd-api/captcha';
79
-
80
- const myCaptcha: CaptchaHandler = {
81
- type: 'my-captcha',
82
- label: 'моей капчи',
83
- // Страница отдаётся вместо настоящей по адресу из `origin`, поэтому виджет видит верный домен.
84
- buildPage: ({ theme }) => `<!doctype html>…<div id="widget" data-theme="${theme}"></div>…`,
85
- // По этому селектору видно, что виджет отрисовался.
86
- widgetReadySelector: '#widget',
87
- // Насколько правее левого края контейнера чекбокс: клик идёт по координатам.
88
- checkboxOffsetX: 30,
89
- readState: (page) =>
90
- page.evaluate(() => {
91
- const field = document.querySelector('#my-token') as { value?: string } | null;
92
- return { token: field?.value || null, error: null };
93
- }),
94
- // Повтор не поможет — например, ключ не разрешён для домена.
95
- isPermanentWidgetError: (code) => code === 'domain-mismatch',
96
- };
97
-
98
- const token = await solveCaptcha(myCaptcha);
99
- ```
74
+ import { solveCaptcha, CaptchaType } from '@itd-api/captcha';
100
75
 
101
- Такой обработчик подставляется и в клиент: `captcha: { type: 'my-captcha', getToken: () =>
102
- solveCaptcha(myCaptcha), field: 'myToken' }`.
76
+ const token = await solveCaptcha(CaptchaType.Itd);
77
+ ```
103
78
 
104
79
  ## Запуск на сервере
105
80
 
@@ -155,20 +130,12 @@ CDP), а не программной установкой значения, —
155
130
  | `browser` | — | Готовый браузер. Тогда пакет его не запускает и не закрывает. |
156
131
  | `launch` | — | Свой запуск браузера. Заменяет все параметры запуска. |
157
132
  | `contextOptions` | `locale: 'ru-RU'`, окно 1280×800 | Настройки контекста. Заменяют стандартные целиком. |
158
- | `logger` | | Функция для вывода хода решения, например `console.debug`. |
159
-
160
- Ключ, назначение токена и адрес виджета настройки конкретного провайдера, поэтому задаются
161
- в его фабрике:
162
-
163
- | Фабрика | Параметр | По умолчанию | Что делает |
164
- | --- | --- | --- | --- |
165
- | `itdCaptcha` | `sitekey` | ключ итд.com | Публичный ключ виджета. |
166
- | `itdCaptcha` | `action` | `'login'` | Назначение токена: `login`, `register`, `password_reset`. |
167
- | `itdCaptcha` | `captchaOrigin` | `https://captcha.xn--d1ah4a.com` | Базовый адрес виджета. |
168
- | `turnstile` | `sitekey` | ключ итд.com | Публичный ключ виджета. |
133
+ | `logger` | логгер клиента | Куда писать ход решения: объект вида `console`. `createCaptchaSolver` без него пишет в логгер `itd-api`. |
134
+ | `type` | — | Какую капчу проходить всегда. Только для `createCaptchaSolver`. |
135
+ | `field` | | Поле тела запроса для закреплённого типа. Только вместе с `type`. |
169
136
 
170
137
  ```ts
171
- await solveCaptcha(itdCaptcha({ action: 'password_reset' }), { timeout: 90_000 });
138
+ createCaptchaSolver({ type: CaptchaType.Itd, timeout: 90_000, headless: true });
172
139
  ```
173
140
 
174
141
  Свой драйвер:
@@ -212,19 +179,52 @@ createCaptchaSolver({
212
179
 
213
180
  ## Совместимость
214
181
 
215
- Виджет пропускает не всякий браузер. Таблица пересобирается скриптом `scripts/drivers.mjs`
216
- из исходников пакета:
182
+ Ниже связки, на которых Cloudflare Turnstile выдал токен. Проверено 19.09.2026
183
+ с окном, если не указано иное.
184
+
185
+ | Драйвер | Версия и конфигурация | Браузер |
186
+ |---|---|---|
187
+ | `patchright` | 1.59.4 · 1.60.2 · 1.61.1, штатные сборки | Chromium 147 · 148 · 149 соответственно |
188
+ | | 1.62.3 · 1.63.0, `executablePath` | Chromium 149 |
189
+ | | 1.63.0, `executablePath` | Chromium 148 |
190
+ | `playwright` | 1.60.0 · 1.61.0, штатные сборки | Chromium 148 · 149 соответственно |
191
+ | | 1.62.1 · 1.63.0, `executablePath` | Chromium 149 |
192
+ | `playwright-core` | 1.62.1 · 1.63.0, `executablePath` | Chromium 149 |
193
+ | `camoufox-js` | 0.11.5 с окном · 0.12.0 с окном и headless, `contextOptions: {}` | Camoufox 152.0.4-beta.29 |
194
+ | `rebrowser-playwright` | 1.48.2 · 1.49.1, `executablePath` | Chromium 149 |
195
+
196
+ <details>
197
+ <summary>Комбинации, не прошедшие проверку</summary>
198
+
199
+ | Драйвер | Версия и конфигурация | Браузер | Результат |
200
+ |---|---|---|---|
201
+ | `patchright` | 1.62.3, штатная сборка | Chromium 151 | таймаут |
202
+ | | 1.63.0, штатная сборка | Chrome for Testing 153 | таймаут |
203
+ | | 1.63.0, `executablePath` | Google Chrome 152 | таймаут |
204
+ | `playwright` | 1.62.1, штатная сборка | Chromium 151 | `600010`, таймаут |
205
+ | | 1.63.0, штатная сборка | Chrome for Testing 153 | `600010`, таймаут |
206
+ | | 1.63.0, `executablePath` | Google Chrome 152 | `600010`, таймаут |
207
+ | `rebrowser-playwright` | 1.52.0, `executablePath` | Chromium 149 | `600010`, таймаут |
208
+ | `playwright-extra` + `puppeteer-extra-plugin-stealth` | 4.3.6 + 2.11.2, `playwright` 1.62.1 · 1.63.0, `executablePath` | Chromium 149 | `600010`, таймаут |
209
+
210
+ </details>
211
+
212
+ Собственная капча ИТД менее требовательна: ее можно получить любым подходящим драйвером и версией браузера.
213
+
214
+ Таблица проверяется скриптом `scripts/drivers.mjs` из исходников пакета:
217
215
 
218
216
  ```sh
219
- npm i --no-save patchright playwright camoufox-js
217
+ npm i --no-save patchright playwright camoufox-js rebrowser-playwright \
218
+ playwright-extra puppeteer-extra-plugin-stealth
220
219
  node scripts/drivers.mjs # Turnstile
221
220
  node scripts/drivers.mjs --itd # капча ИТД
222
221
  ```
223
222
 
224
- Всё, что удаётся получить с окном; `headless: true` токена почти нигде не даёт. Сборка
225
- приезжает вместе с версией драйвера, ею и выбирается `npm i playwright@1.61`. Поставленная
226
- отдельно тоже годится: `npx @puppeteer/browsers install chrome@149.0.7827.55` и путь
227
- в `executablePath`. Версию запущенного браузера пакет называет в сообщении о таймауте.
223
+ Штатная сборка приезжает вместе с версией драйвера. Другую можно поставить командой
224
+ `npx @puppeteer/browsers install chrome@149.0.7827.55` и передать путь в `executablePath`.
225
+ Версия драйвера сама по себе не гарантирует результат: например, `patchright` и
226
+ `playwright` 1.63.0 не проходят Turnstile на штатном Chrome for Testing 153, но проходят
227
+ на Chromium 149 через `executablePath`.
228
228
 
229
229
  ## Лицензия
230
230
 
package/dist/index.cjs CHANGED
@@ -54,9 +54,8 @@ const DEFAULT_ARGS = ["--disable-dev-shm-usage", "--disable-blink-features=Autom
54
54
  /**
55
55
  * Драйверы в порядке предпочтения.
56
56
  *
57
- * `patchright` впереди намеренно: он ставит собственную сборку Chromium, и та проходит
58
- * виджет там, где сборка из свежего Playwright уже нет. Если стоит только `playwright`,
59
- * ничего не меняется — берётся он.
57
+ * `patchright` впереди намеренно: проверенная связка `patchright@1.61.1` со штатным
58
+ * Chromium 149 проходит виджет. Если стоит только `playwright`, берётся он.
60
59
  */
61
60
  const DRIVERS = [
62
61
  "patchright",
@@ -101,7 +100,7 @@ async function loadDriver(requested) {
101
100
  } catch (error) {
102
101
  if (!isModuleNotFound(error)) throw error;
103
102
  }
104
- throw new CaptchaError(CaptchaFailure.DriverMissing, requested ? `Драйвер ${requested} не установлен.` : "Не найден драйвер браузера. Установите его командой: npm i patchright && npx patchright install chromium. Либо передайте свой запуск браузера через параметр launch.");
103
+ throw new CaptchaError(CaptchaFailure.DriverMissing, requested ? `Драйвер ${requested} не установлен.` : "Не найден драйвер браузера. Установите проверенную связку командой: npm i patchright@1.61.1 && npx patchright install chromium. Либо передайте свой запуск браузера через параметр launch.");
105
104
  }
106
105
  /** Собирает параметры запуска, не поднимая браузер. @internal */
107
106
  function resolveLaunchOptions(options, driver = "") {
@@ -140,6 +139,12 @@ async function launchBrowser(options) {
140
139
  //#region src/options.ts
141
140
  /** Базовый URL сайта итд.com. Домен записан в punycode: `итд.com`. */
142
141
  const DEFAULT_ORIGIN = "https://xn--d1ah4a.com";
142
+ const LOGGER_METHODS = [
143
+ "debug",
144
+ "info",
145
+ "warn",
146
+ "error"
147
+ ];
143
148
  /**
144
149
  * Приводит адрес к корню и проверяет его пригодность.
145
150
  *
@@ -172,7 +177,10 @@ function resolveOptions(options) {
172
177
  if (theme !== "auto" && theme !== "light" && theme !== "dark") throw new TypeError("theme должен быть 'auto', 'light' или 'dark'");
173
178
  for (const [name, value] of [["headless", options.headless], ["disableSandbox", options.disableSandbox]]) if (value !== void 0 && typeof value !== "boolean") throw new TypeError(`${name} должен быть boolean`);
174
179
  for (const [name, value] of [["driver", options.driver], ["channel", options.channel]]) if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new TypeError(`${name} должен быть непустой строкой`);
175
- if (options.logger !== void 0 && typeof options.logger !== "function") throw new TypeError("logger должен быть функцией");
180
+ if (options.logger !== void 0) {
181
+ if (typeof options.logger !== "object" || options.logger === null) throw new TypeError("logger должен быть объектом с методами debug, info, warn и error");
182
+ for (const method of LOGGER_METHODS) if (typeof options.logger[method] !== "function") throw new TypeError(`logger.${method} должен быть функцией`);
183
+ }
176
184
  if (options.contextOptions !== void 0 && (typeof options.contextOptions !== "object" || options.contextOptions === null || Array.isArray(options.contextOptions))) throw new TypeError("contextOptions должен быть объектом");
177
185
  if (options.launch !== void 0 && typeof options.launch !== "function") throw new TypeError("launch должен быть функцией");
178
186
  if (options.args !== void 0 && (!Array.isArray(options.args) || options.args.some((argument) => typeof argument !== "string"))) throw new TypeError("args должен быть массивом строк");
@@ -521,7 +529,8 @@ async function clickCheckbox(page, offsetX) {
521
529
  }
522
530
  /** Ждёт токен, периодически подталкивая виджет кликом. */
523
531
  async function waitForToken(page, handler, options, browser) {
524
- const deadline = Date.now() + options.timeout;
532
+ const startedAt = Date.now();
533
+ const deadline = startedAt + options.timeout;
525
534
  await page.waitForSelector(handler.widgetReadySelector, {
526
535
  timeout: Math.min(WIDGET_APPEAR_TIMEOUT, options.timeout),
527
536
  state: "attached"
@@ -531,7 +540,7 @@ async function waitForToken(page, handler, options, browser) {
531
540
  while (Date.now() < deadline) {
532
541
  const state = await handler.readState(page);
533
542
  if (state.token) {
534
- options.logger?.("токен получен");
543
+ options.logger?.info(`токен ${handler.label} получен за ${Date.now() - startedAt} мс`);
535
544
  return state.token;
536
545
  }
537
546
  if (state.error && state.error !== lastError) {
@@ -540,10 +549,10 @@ async function waitForToken(page, handler, options, browser) {
540
549
  widgetCode: state.error
541
550
  });
542
551
  lastError = state.error;
543
- options.logger?.(`виджет сообщил об ошибке ${state.error}, ждём повтора`);
552
+ options.logger?.debug(`виджет ${handler.label} сообщил об ошибке ${state.error}, ждём повтора`);
544
553
  }
545
554
  if (Date.now() >= nextClickAt && await clickCheckbox(page, handler.checkboxOffsetX)) {
546
- options.logger?.("клик по чекбоксу");
555
+ options.logger?.debug(`клик по чекбоксу ${handler.label}`);
547
556
  nextClickAt = Date.now() + CLICK_INTERVAL;
548
557
  }
549
558
  await sleep(POLL_INTERVAL);
@@ -609,16 +618,17 @@ function toCaptchaError(error, handler) {
609
618
  * @throws {CaptchaError} если токен получить не удалось
610
619
  */
611
620
  async function runSolver(handler, options) {
612
- const browser = options.browser ?? await launchBrowser(options);
613
621
  const owned = options.browser === void 0;
622
+ if (owned) options.logger?.info(`запуск браузера для ${handler.label}`);
623
+ const browser = options.browser ?? await launchBrowser(options);
614
624
  try {
615
625
  for (let attempt = 1; attempt <= options.attempts; attempt++) try {
616
- options.logger?.(`попытка ${attempt} из ${options.attempts}`);
626
+ options.logger?.debug(`попытка ${attempt} из ${options.attempts} для ${handler.label}`);
617
627
  return await solveOnce(browser, handler, options);
618
628
  } catch (raw) {
619
629
  const error = toCaptchaError(raw, handler);
620
630
  if (isPermanent(error) || attempt === options.attempts) throw error;
621
- options.logger?.(`попытка ${attempt} не удалась: ${error instanceof Error ? error.message : String(error)}`);
631
+ options.logger?.warn(`попытка ${attempt} из ${options.attempts} для ${handler.label} не удалась (${error instanceof Error ? error.message : String(error)}), повтор`);
622
632
  }
623
633
  throw new CaptchaError(CaptchaFailure.Timeout, `Токен ${handler.label} получить не удалось`, { type: handler.type });
624
634
  } finally {
@@ -645,30 +655,46 @@ async function runSolver(handler, options) {
645
655
  async function solveCaptcha(target, options = {}) {
646
656
  return runSolver(resolveHandler(target), resolveOptions(options));
647
657
  }
658
+ /** Без своего логгера источник пишет ход решения в логгер клиента. */
659
+ function withClientLogger(options, context) {
660
+ if (options.logger || !context?.logger) return options;
661
+ return {
662
+ ...options,
663
+ logger: context.logger
664
+ };
665
+ }
648
666
  /**
649
- * Собирает источник токена для клиента `itd-api`.
667
+ * Собирает источник токена капчи для клиента `itd-api`.
650
668
  *
651
669
  * Токен одноразовый и живёт несколько минут, поэтому клиент спрашивает его заново перед
652
- * каждым входом. Браузер поднимается на время одного вызова и сразу закрывается.
670
+ * каждым запросом, которому нужна капча. Браузер поднимается на время одного вызова
671
+ * и сразу закрывается.
672
+ *
673
+ * @throws {TypeError} если тип капчи незнаком или `field` задан без него
653
674
  *
654
675
  * @example
655
676
  * ```ts
656
677
  * import { ItdClient } from 'itd-api';
657
678
  * import { FileTokenStorage } from 'itd-api/node';
658
- * import { createCaptchaSolver } from '@itd-api/captcha';
679
+ * import { createCaptchaSolver, CaptchaType } from '@itd-api/captcha';
659
680
  *
660
681
  * const itd = new ItdClient({
661
682
  * storage: new FileTokenStorage('./.itd-session.json'),
662
- * auth: {
663
- * email: process.env.ITD_EMAIL!,
664
- * password: process.env.ITD_PASSWORD!,
665
- * captcha: createCaptchaSolver(),
666
- * },
683
+ * auth: { email: process.env.ITD_EMAIL!, password: process.env.ITD_PASSWORD! },
684
+ * captcha: createCaptchaSolver({ type: CaptchaType.Cloudflare }),
667
685
  * });
668
686
  * ```
669
687
  */
670
688
  function createCaptchaSolver(options = {}) {
671
- return { getToken: (type) => solveCaptcha(type, options) };
689
+ const { type, field, ...solveOptions } = options;
690
+ if (type === void 0) return { getToken: (requested, context) => solveCaptcha(requested, withClientLogger(solveOptions, context)) };
691
+ const handler = resolveHandler(type);
692
+ const resolved = resolveOptions(solveOptions);
693
+ return {
694
+ type: handler.type,
695
+ ...field === void 0 ? {} : { field },
696
+ getToken: (_requested, context) => runSolver(handler, withClientLogger(resolved, context))
697
+ };
672
698
  }
673
699
  //#endregion
674
700
  exports.CaptchaError = CaptchaError;