itd-api 0.9.3 → 0.10.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 (36) hide show
  1. package/README.md +9 -8
  2. package/dist/events/index.cjs +2 -2
  3. package/dist/events/index.d.cts +2 -2
  4. package/dist/events/index.d.ts +2 -2
  5. package/dist/events/index.js +2 -2
  6. package/dist/index.cjs +452 -42
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +229 -49
  9. package/dist/index.d.ts +229 -49
  10. package/dist/index.js +448 -44
  11. package/dist/index.js.map +1 -1
  12. package/dist/rest/index.cjs +2 -2
  13. package/dist/rest/index.d.cts +2 -2
  14. package/dist/rest/index.d.ts +2 -2
  15. package/dist/rest/index.js +2 -2
  16. package/dist/shared/{events-BPxBF0gQ.d.cts → events-CZXU16qU.d.cts} +2 -2
  17. package/dist/shared/{events-BzjI6pn5.js → events-Cipe-Fm9.js} +3 -3
  18. package/dist/shared/{events-BzjI6pn5.js.map → events-Cipe-Fm9.js.map} +1 -1
  19. package/dist/shared/{events-DnwfcLxS.d.ts → events-D3CSlpXR.d.ts} +2 -2
  20. package/dist/shared/{events-C1FudmSe.cjs → events-MtRNe_TF.cjs} +8 -2
  21. package/dist/shared/{events-C1FudmSe.cjs.map → events-MtRNe_TF.cjs.map} +1 -1
  22. package/dist/shared/{render-uFW2kDxp.d.ts → render-Cs6AOXbD.d.ts} +2 -2
  23. package/dist/shared/{render-CtOpp9Pm.js → render-CubTTof_.js} +2 -2
  24. package/dist/shared/{render-CtOpp9Pm.js.map → render-CubTTof_.js.map} +1 -1
  25. package/dist/shared/{render-C1F_QWYD.d.cts → render-bZwOk3CC.d.cts} +2 -2
  26. package/dist/shared/{render-Omy1drH1.cjs → render-dscKp9CC.cjs} +2 -2
  27. package/dist/shared/{render-Omy1drH1.cjs.map → render-dscKp9CC.cjs.map} +1 -1
  28. package/dist/shared/{url-3Ts6EEHQ.js → url-CntTEAxu.js} +65 -3
  29. package/dist/shared/url-CntTEAxu.js.map +1 -0
  30. package/dist/shared/{url-D6wsXm1T.cjs → url-DgTYZN_v.cjs} +100 -2
  31. package/dist/shared/url-DgTYZN_v.cjs.map +1 -0
  32. package/dist/shared/{url-DubjkRR9.d.cts → url-Dum7r_FT.d.cts} +70 -3
  33. package/dist/shared/{url-DubjkRR9.d.ts → url-Dum7r_FT.d.ts} +70 -3
  34. package/package.json +2 -1
  35. package/dist/shared/url-3Ts6EEHQ.js.map +0 -1
  36. package/dist/shared/url-D6wsXm1T.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_url = require("./shared/url-D6wsXm1T.cjs");
2
+ const require_url = require("./shared/url-DgTYZN_v.cjs");
3
3
  const require_errors = require("./shared/errors-BmP3TKoW.cjs");
4
4
  const require_redact = require("./shared/redact-lAmck1Z6.cjs");
5
- const require_render = require("./shared/render-Omy1drH1.cjs");
5
+ const require_render = require("./shared/render-dscKp9CC.cjs");
6
6
  const require_options = require("./shared/options-DjEtbVaf.cjs");
7
- const require_events = require("./shared/events-C1FudmSe.cjs");
7
+ const require_events = require("./shared/events-MtRNe_TF.cjs");
8
8
  const require_key_value_store = require("./shared/key-value-store-B5GYVEYZ.cjs");
9
9
  const require_storage = require("./shared/storage-CZJZoaz9.cjs");
10
10
  const require_multi_storage = require("./shared/multi-storage-s-fPtjoy.cjs");
@@ -32,12 +32,37 @@ const SignInStatus = Object.freeze({
32
32
  Authenticated: "authenticated",
33
33
  OtpRequired: "otp_required"
34
34
  });
35
+ /**
36
+ * Поле тела запроса по умолчанию для известных провайдеров.
37
+ *
38
+ * Используется, когда провайдера не спрашивали. Ответ `captchaProvider()` важнее этой таблицы.
39
+ */
40
+ const CAPTCHA_FIELDS = Object.freeze({
41
+ [require_url.CaptchaType.Itd]: require_url.CaptchaField.Itd,
42
+ [require_url.CaptchaType.Cloudflare]: require_url.CaptchaField.Cloudflare
43
+ });
44
+ /** Состояния, которые может вернуть проверка QR-входа. */
45
+ const QrLoginStatus = Object.freeze({
46
+ Pending: "pending",
47
+ Scanned: "scanned",
48
+ CaptchaRequired: "captcha_required",
49
+ Authorized: "authorized",
50
+ Rejected: "rejected"
51
+ });
52
+ /** Состояния, приходящие из потокового наблюдения за QR-входом. */
53
+ const QrLoginStreamStatus = Object.freeze({
54
+ Pending: "pending",
55
+ Scanned: "scanned",
56
+ Approved: "approved",
57
+ Rejected: "rejected"
58
+ });
35
59
  function requireString(body, key, message) {
36
60
  const value = require_url.pickString(body, key);
37
61
  if (!value) throw new require_errors.ItdConfigError(message);
38
62
  return value;
39
63
  }
40
64
  const AUTH_CHECK = require_url.passthroughOperation("auth.check");
65
+ const AUTH_CAPTCHA_PROVIDER = require_url.passthroughOperation("auth.captchaProvider");
41
66
  const AUTH_SIGN_UP = require_url.defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
42
67
  const AUTH_SIGN_IN = require_url.defineBuiltInOperation("auth.signIn", (body) => {
43
68
  const accessToken = require_url.pickString(body, "accessToken");
@@ -59,6 +84,96 @@ const AUTH_CHANGE_PASSWORD = require_url.voidOperation("auth.changePassword");
59
84
  const AUTH_SESSIONS = require_url.defineBuiltInOperation("auth.sessions", (body) => require_url.pickArray(body, "sessions"));
60
85
  const AUTH_REVOKE_SESSION = require_url.voidOperation("auth.revokeSession");
61
86
  const AUTH_REVOKE_OTHER_SESSIONS = require_url.voidOperation("auth.revokeOtherSessions");
87
+ const AUTH_QR_START = require_url.passthroughOperation("auth.qrStart");
88
+ const AUTH_QR_CLAIM = require_url.passthroughOperation("auth.qrClaim");
89
+ //#endregion
90
+ //#region src/session/captcha.ts
91
+ function requireCaptchaText(value, field) {
92
+ if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new require_errors.ItdConfigError(`${field} должен быть непустой строкой`);
93
+ }
94
+ /**
95
+ * Проверяет форму источника токена и приводит его к объекту.
96
+ *
97
+ * Сам объект сохраняется как есть: `getToken` вызывается на нём, чтобы источнику остался
98
+ * доступен собственный `this`.
99
+ *
100
+ * @throws {ItdConfigError} при некорректных значениях
101
+ */
102
+ function resolveCaptchaSolver(captcha) {
103
+ if (captcha === void 0) return void 0;
104
+ if (typeof captcha === "function") return { getToken: captcha };
105
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("captcha должен быть функцией либо объектом с getToken");
106
+ const { getToken, type, field } = captcha;
107
+ if (typeof getToken !== "function") throw new require_errors.ItdConfigError("captcha.getToken должен быть функцией");
108
+ requireCaptchaText(type, "captcha.type");
109
+ requireCaptchaText(field, "captcha.field");
110
+ return captcha;
111
+ }
112
+ /**
113
+ * Проверяет форму готового токена капчи.
114
+ *
115
+ * @param path имя опции или аргумента для текста ошибки
116
+ * @throws {ItdConfigError} если токен или его тип не указаны
117
+ */
118
+ function validateCaptchaToken(captcha, path) {
119
+ if (captcha === void 0) return;
120
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError(`${path} должен быть объектом { type, token }`);
121
+ const { type, token, field } = captcha;
122
+ if (typeof token !== "string" || token.trim() === "") throw new require_errors.ItdConfigError(`${path}.token должен быть непустой строкой`);
123
+ if (typeof type !== "string" || type.trim() === "") throw new require_errors.ItdConfigError(`${path}.type должен быть непустой строкой`);
124
+ requireCaptchaText(field, `${path}.field`);
125
+ }
126
+ /** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
127
+ function readCaptchaProvider(body) {
128
+ const config = require_url.isRecord$1(body) ? body : void 0;
129
+ const provider = config?.provider;
130
+ const field = config?.field;
131
+ if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new require_errors.ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
132
+ return {
133
+ provider,
134
+ field
135
+ };
136
+ }
137
+ /** Куда класть токен: настройка, иначе умолчание провайдера. */
138
+ function resolveField(type, configured) {
139
+ const field = configured ?? CAPTCHA_FIELDS[type];
140
+ if (field) return field;
141
+ throw new require_errors.ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте captcha.field либо оставьте captcha.type = ${require_url.CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
142
+ }
143
+ /** Спрашивает токен у источника и убеждается, что он непустой. */
144
+ async function requireSolvedToken(solver, type) {
145
+ const token = await solver.getToken(type);
146
+ if (typeof token === "string" && token.trim() !== "") return token;
147
+ throw new require_errors.ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте captcha.getToken.`);
148
+ }
149
+ /**
150
+ * Готовит фрагмент тела запроса, взяв токен у источника.
151
+ *
152
+ * При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
153
+ * и имя поля приходят оттуда. Явно названный тип обходится без этого запроса.
154
+ *
155
+ * @param askProvider как спросить активного провайдера; вызывается, только если это нужно
156
+ * @throws {ItdConfigError} если токен получить не удалось
157
+ */
158
+ async function solveCaptchaBody(solver, askProvider) {
159
+ const choice = solver.type ?? require_url.CaptchaChoice.Auto;
160
+ if (choice === require_url.CaptchaChoice.Auto) {
161
+ const { provider, field } = await askProvider();
162
+ return { [field]: await requireSolvedToken(solver, provider) };
163
+ }
164
+ return { [resolveField(choice, solver.field)]: await requireSolvedToken(solver, choice) };
165
+ }
166
+ /**
167
+ * Готовит фрагмент тела из готового токена.
168
+ *
169
+ * Поле берётся из самого токена либо из умолчания провайдера; запроса к серверу нет.
170
+ *
171
+ * @throws {ItdConfigError} если токен или его тип не указаны
172
+ */
173
+ function captchaBody(captcha) {
174
+ validateCaptchaToken(captcha, "captcha");
175
+ return { [resolveField(captcha.type, captcha.field)]: captcha.token };
176
+ }
62
177
  //#endregion
63
178
  //#region src/session/jwt.ts
64
179
  /**
@@ -131,11 +246,10 @@ function validateAuth(auth) {
131
246
  return { ...auth };
132
247
  }
133
248
  if ("email" in auth || "password" in auth) {
134
- const { email, password, turnstileToken, getTurnstileToken } = auth;
249
+ const { email, password, captcha } = auth;
135
250
  if (typeof email !== "string" || email.trim() === "") throw new require_errors.ItdConfigError("auth.email должен быть непустой строкой");
136
251
  if (typeof password !== "string" || password === "") throw new require_errors.ItdConfigError("auth.password должен быть непустой строкой");
137
- if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") throw new require_errors.ItdConfigError("auth.getTurnstileToken должен быть функцией");
138
- if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new require_errors.ItdConfigError("auth.turnstileToken должен быть непустой строкой");
252
+ validateCaptchaToken(captcha, "auth.captcha");
139
253
  return { ...auth };
140
254
  }
141
255
  throw new require_errors.ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
@@ -164,6 +278,7 @@ function resolveSessionConfig(options, runtime) {
164
278
  return {
165
279
  ...runtime,
166
280
  auth: validateAuth(options.auth),
281
+ captcha: resolveCaptchaSolver(options.captcha),
167
282
  storage: resolveStorage(options.storage),
168
283
  deviceId: options.deviceId,
169
284
  autoRefresh: options.autoRefresh ?? true,
@@ -182,6 +297,7 @@ function createItdAuth(options, deps) {
182
297
  }
183
298
  /** Пути эндпоинтов авторизации. */
184
299
  const AUTH_PATHS = {
300
+ captchaProvider: "/api/v1/auth/captcha/provider",
185
301
  signUp: "/api/v1/auth/sign-up",
186
302
  signIn: "/api/v1/auth/sign-in",
187
303
  verifyOtp: "/api/v1/auth/verify-otp",
@@ -191,7 +307,10 @@ const AUTH_PATHS = {
191
307
  forgotPassword: "/api/v1/auth/forgot-password",
192
308
  resetPassword: "/api/v1/auth/reset-password",
193
309
  changePassword: "/api/v1/auth/change-password",
194
- sessions: "/api/v1/auth/sessions"
310
+ sessions: "/api/v1/auth/sessions",
311
+ qrStart: "/api/v1/auth/qr/start",
312
+ qrClaim: "/api/v1/auth/qr/claim",
313
+ qrStream: "/api/v1/auth/qr/stream"
195
314
  };
196
315
  /**
197
316
  * Публичный ключ Cloudflare Turnstile платформы итд.com.
@@ -201,14 +320,15 @@ const AUTH_PATHS = {
201
320
  *
202
321
  * Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
203
322
  * Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
204
- * подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
323
+ * подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
205
324
  * без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
206
325
  *
207
326
  * @example
208
327
  * ```ts
209
328
  * turnstile.render('#captcha', {
210
329
  * sitekey: TURNSTILE_SITE_KEY,
211
- * callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
330
+ * callback: (token) =>
331
+ * itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
212
332
  * });
213
333
  * ```
214
334
  */
@@ -330,6 +450,8 @@ var AuthManager = class {
330
450
  */
331
451
  #authEpoch = 0;
332
452
  #externalReadGeneration = 0;
453
+ /** Разовый токен из `auth.captcha` тратится ровно на один запрос. */
454
+ #configCaptchaSpent = false;
333
455
  #disposed = false;
334
456
  #persistence = Promise.resolve();
335
457
  constructor(config, http, jar, hooks = {}) {
@@ -626,6 +748,13 @@ var AuthManager = class {
626
748
  }
627
749
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
628
750
  async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
751
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
752
+ }
753
+ /** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
754
+ async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
755
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
756
+ }
757
+ async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
629
758
  this.#assertActive();
630
759
  if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
631
760
  await this.#loadSession();
@@ -635,7 +764,7 @@ var AuthManager = class {
635
764
  this.#invalidateInFlight();
636
765
  this.#transitionAuth(accessToken);
637
766
  const saved = this.#saveSession({
638
- ...this.#session ?? {},
767
+ ...replaceSession ? {} : this.#session ?? {},
639
768
  accessToken,
640
769
  obtainedAt: this.#config.clock.now()
641
770
  });
@@ -851,28 +980,50 @@ var AuthManager = class {
851
980
  return promise;
852
981
  }
853
982
  /**
854
- * Берёт токен капчи для входа.
983
+ * Готовит фрагмент тела с токеном капчи для запроса, который её требует.
855
984
  *
856
- * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
857
- * несколько минут, поэтому при повторном входе через сутки годится только свежий.
985
+ * Источники в порядке убывания приоритета: токен, переданный в сам вызов; разовый токен
986
+ * из `auth.captcha`; опция `captcha`. Когда ни одного нет, фрагмент пустой: нужна ли
987
+ * капча этому запросу, решает сервер.
988
+ *
989
+ * @param explicit готовый токен, переданный в вызов
990
+ * @throws {ItdConfigError} если токен неполный или источник его не вернул
858
991
  */
859
- async #resolveTurnstileToken(credentials) {
860
- if (credentials.getTurnstileToken) {
861
- const token = await credentials.getTurnstileToken();
862
- if (token) return token;
863
- }
864
- if (credentials.turnstileToken) return credentials.turnstileToken;
865
- throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
992
+ async captchaBody(explicit) {
993
+ if (explicit) return captchaBody(explicit);
994
+ const configured = this.#takeConfiguredCaptcha();
995
+ if (configured) return captchaBody(configured);
996
+ const solver = this.#config.captcha;
997
+ if (solver) return solveCaptchaBody(solver, () => this.#resolveCaptchaProvider());
998
+ this.#config.logger?.debug("Запрос уходит без токена капчи: не передан аргумент captcha и не задана опция captcha");
999
+ return {};
1000
+ }
1001
+ /** Отдаёт разовый токен из `auth.captcha`, помечая его потраченным. */
1002
+ #takeConfiguredCaptcha() {
1003
+ if (this.#configCaptchaSpent) return void 0;
1004
+ const auth = this.#config.auth;
1005
+ const captcha = require_url.isRecord$1(auth) ? auth.captcha : void 0;
1006
+ if (!captcha) return void 0;
1007
+ this.#configCaptchaSpent = true;
1008
+ return captcha;
1009
+ }
1010
+ /** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
1011
+ async #resolveCaptchaProvider() {
1012
+ return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
1013
+ path: AUTH_PATHS.captchaProvider,
1014
+ skipAuth: true,
1015
+ skipAuthRefresh: true
1016
+ }));
866
1017
  }
867
1018
  async #performSignIn(credentials, revision) {
868
- const turnstileToken = await this.#resolveTurnstileToken(credentials);
1019
+ const captcha = await this.captchaBody();
869
1020
  const flowCookies = new require_redact.CookieJar();
870
1021
  const result = await this.#http.execute(AUTH_SIGN_IN, {
871
1022
  path: AUTH_PATHS.signIn,
872
1023
  body: {
873
1024
  email: credentials.email,
874
1025
  password: credentials.password,
875
- turnstileToken
1026
+ ...captcha
876
1027
  },
877
1028
  skipAuth: true,
878
1029
  skipAuthRefresh: true,
@@ -896,6 +1047,125 @@ var AuthManager = class {
896
1047
  }
897
1048
  };
898
1049
  //#endregion
1050
+ //#region src/session/qr-stream.ts
1051
+ const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
1052
+ function readQrLoginStreamEvent(data) {
1053
+ let value;
1054
+ try {
1055
+ value = JSON.parse(data);
1056
+ } catch {
1057
+ return;
1058
+ }
1059
+ if (!require_url.isRecord$1(value)) return void 0;
1060
+ const event = value;
1061
+ if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
1062
+ return {
1063
+ status: event.status,
1064
+ ...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
1065
+ };
1066
+ }
1067
+ /** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
1068
+ async function consumeQrLoginStream(body, onEvent, signal) {
1069
+ const reader = body.getReader();
1070
+ const decoder = new TextDecoder();
1071
+ let delivery = Promise.resolve();
1072
+ let completed = false;
1073
+ const cancel = () => {
1074
+ reader.cancel(signal?.reason).catch(() => {});
1075
+ };
1076
+ signal?.addEventListener("abort", cancel, { once: true });
1077
+ const parser = require_events.createParser({ onEvent(message) {
1078
+ const event = readQrLoginStreamEvent(message.data);
1079
+ if (!event) return;
1080
+ delivery = delivery.then(() => onEvent(event));
1081
+ } });
1082
+ try {
1083
+ for (;;) {
1084
+ const { done, value } = await reader.read();
1085
+ if (done) break;
1086
+ parser.feed(decoder.decode(value, { stream: true }));
1087
+ await delivery;
1088
+ }
1089
+ parser.feed(decoder.decode());
1090
+ await delivery;
1091
+ if (signal?.aborted) throw signal.reason;
1092
+ completed = true;
1093
+ } finally {
1094
+ signal?.removeEventListener("abort", cancel);
1095
+ if (!completed) await reader.cancel().catch(() => {});
1096
+ reader.releaseLock?.();
1097
+ }
1098
+ }
1099
+ /** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
1100
+ async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
1101
+ connection.assertActive?.("открыть QR-поток");
1102
+ const path = AUTH_PATHS.qrStream;
1103
+ const method = "POST";
1104
+ const url = require_redact.joinUrl(connection.baseUrl, path);
1105
+ const abort = require_url.createRequestAbortScope(signal, connection.signal, 0, connection.clock);
1106
+ try {
1107
+ const headers = await connection.baseHeaders(url);
1108
+ headers.delete("Authorization");
1109
+ headers.delete("Cookie");
1110
+ headers.set("Accept", "text/event-stream");
1111
+ headers.set("Content-Type", "application/json");
1112
+ const cookie = cookies.getHeader(url);
1113
+ if (cookie) headers.set("Cookie", cookie);
1114
+ let response;
1115
+ try {
1116
+ response = await connection.fetch(url, {
1117
+ method,
1118
+ headers,
1119
+ body: JSON.stringify(input),
1120
+ credentials: "include",
1121
+ signal: abort.signal
1122
+ });
1123
+ } catch (error) {
1124
+ const failure = new require_errors.ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
1125
+ method,
1126
+ path,
1127
+ cause: error
1128
+ });
1129
+ throw require_url.requestAbortError(abort, {
1130
+ timeout: 0,
1131
+ method,
1132
+ path
1133
+ }, failure);
1134
+ }
1135
+ cookies.setFromResponse(response.url || url, response);
1136
+ if (!response.ok) {
1137
+ const text = await response.text();
1138
+ let body = text;
1139
+ try {
1140
+ body = JSON.parse(text);
1141
+ } catch {}
1142
+ throw require_url.createApiError({
1143
+ method,
1144
+ path,
1145
+ status: response.status,
1146
+ statusText: response.statusText,
1147
+ headers: response.headers,
1148
+ response,
1149
+ body,
1150
+ now: connection.clock.now()
1151
+ });
1152
+ }
1153
+ if (!response.body) throw new require_errors.ItdNetworkError("Сервер не вернул тело QR-потока", {
1154
+ method,
1155
+ path
1156
+ });
1157
+ await consumeQrLoginStream(response.body, onEvent, abort.signal);
1158
+ } catch (error) {
1159
+ throw require_url.requestAbortError(abort, {
1160
+ timeout: 0,
1161
+ method,
1162
+ path
1163
+ }, error);
1164
+ } finally {
1165
+ abort.cleanup();
1166
+ }
1167
+ }
1168
+ //#endregion
899
1169
  //#region src/resources/auth.ts
900
1170
  /**
901
1171
  * Результат входа.
@@ -910,9 +1180,36 @@ var AuthManager = class {
910
1180
  */
911
1181
  var AuthResource = class extends require_render.BaseResource {
912
1182
  #auth;
1183
+ #connection;
1184
+ #qrFlows = /* @__PURE__ */ new Map();
913
1185
  constructor(http, deps) {
914
1186
  super(http);
915
1187
  this.#auth = deps.auth;
1188
+ this.#connection = deps.connection;
1189
+ }
1190
+ #pruneQrFlows() {
1191
+ const now = this.#connection.clock.now();
1192
+ for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
1193
+ }
1194
+ #qrFlow(qrId) {
1195
+ this.#pruneQrFlows();
1196
+ const existing = this.#qrFlows.get(qrId);
1197
+ if (existing) return existing;
1198
+ const flow = {
1199
+ cookies: this.#auth.createCookieFlow(false),
1200
+ expiresAt: void 0,
1201
+ captchaRequired: false
1202
+ };
1203
+ this.#qrFlows.set(qrId, flow);
1204
+ return flow;
1205
+ }
1206
+ #touchQrFlow(flow, expiresIn) {
1207
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
1208
+ const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
1209
+ flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
1210
+ }
1211
+ #forgetTerminalQrError(qrId, error) {
1212
+ if (require_errors.isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
916
1213
  }
917
1214
  /**
918
1215
  * Проверяет состояние авторизации и возвращает текущего пользователя.
@@ -926,14 +1223,109 @@ var AuthResource = class extends require_render.BaseResource {
926
1223
  });
927
1224
  }
928
1225
  /**
1226
+ * Узнаёт активного провайдера капчи и поле токена.
1227
+ *
1228
+ * Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
1229
+ * Автоматическому входу этот вызов не нужен — клиент делает его сам.
1230
+ */
1231
+ captchaProvider(options = {}) {
1232
+ return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
1233
+ path: AUTH_PATHS.captchaProvider,
1234
+ skipAuth: true,
1235
+ skipAuthRefresh: true,
1236
+ ...options
1237
+ });
1238
+ }
1239
+ /** Создаёт короткоживущую сессию QR-входа. */
1240
+ async startQrLogin(options = {}) {
1241
+ const cookies = this.#auth.createCookieFlow(false);
1242
+ const result = await this.http.execute(AUTH_QR_START, {
1243
+ path: AUTH_PATHS.qrStart,
1244
+ skipAuth: true,
1245
+ skipAuthRefresh: true,
1246
+ cookieJar: cookies,
1247
+ ...options
1248
+ });
1249
+ if (typeof result.qrId === "string" && result.qrId !== "") {
1250
+ const flow = {
1251
+ cookies,
1252
+ expiresAt: void 0,
1253
+ captchaRequired: false
1254
+ };
1255
+ this.#touchQrFlow(flow, result.expiresIn);
1256
+ this.#qrFlows.set(result.qrId, flow);
1257
+ }
1258
+ return result;
1259
+ }
1260
+ /**
1261
+ * Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
1262
+ *
1263
+ * До подтверждения возвращает промежуточный статус. Токен капчи добывается, только когда
1264
+ * сервер о ней попросил — статусом `captcha_required` на прошлой проверке, — либо когда
1265
+ * передан в сам вызов. Полученный `accessToken` автоматически сохраняется в клиенте.
1266
+ */
1267
+ async claimQrLogin(input, options = {}) {
1268
+ const revision = this.#auth.revision();
1269
+ const flow = this.#qrFlow(input.qrId);
1270
+ try {
1271
+ const { captcha, ...rest } = input;
1272
+ const needsCaptcha = captcha !== void 0 || flow.captchaRequired;
1273
+ const result = await this.http.execute(AUTH_QR_CLAIM, {
1274
+ path: AUTH_PATHS.qrClaim,
1275
+ body: {
1276
+ ...rest,
1277
+ ...needsCaptcha ? await this.#auth.captchaBody(captcha) : {}
1278
+ },
1279
+ skipAuth: true,
1280
+ skipAuthRefresh: true,
1281
+ cookieJar: flow.cookies,
1282
+ ...options
1283
+ });
1284
+ this.#touchQrFlow(flow, result.expiresIn);
1285
+ if (result.status === QrLoginStatus.CaptchaRequired) flow.captchaRequired = true;
1286
+ if (result.status === QrLoginStatus.Authorized) {
1287
+ await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
1288
+ this.#qrFlows.delete(input.qrId);
1289
+ } else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
1290
+ return result;
1291
+ } catch (error) {
1292
+ this.#forgetTerminalQrError(input.qrId, error);
1293
+ throw error;
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
1298
+ *
1299
+ * Событие `approved` означает, что подтверждение завершено: после него вызовите
1300
+ * {@link claimQrLogin}, чтобы получить и сохранить access token.
1301
+ */
1302
+ async streamQrLogin(input, onEvent, options = {}) {
1303
+ if (typeof onEvent !== "function") throw new require_errors.ItdConfigError("onEvent QR-потока должен быть функцией");
1304
+ const flow = this.#qrFlow(input.qrId);
1305
+ try {
1306
+ await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
1307
+ this.#touchQrFlow(flow, event.expiresIn);
1308
+ if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
1309
+ await onEvent(event);
1310
+ }, options.signal);
1311
+ } catch (error) {
1312
+ this.#forgetTerminalQrError(input.qrId, error);
1313
+ throw error;
1314
+ }
1315
+ }
1316
+ /**
929
1317
  * Регистрирует аккаунт и запускает подтверждение по коду.
930
1318
  *
931
1319
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
932
1320
  */
933
- signUp(credentials, options = {}) {
1321
+ async signUp(credentials, options = {}) {
1322
+ const { captcha, ...rest } = credentials;
934
1323
  return this.http.execute(AUTH_SIGN_UP, {
935
1324
  path: AUTH_PATHS.signUp,
936
- body: credentials,
1325
+ body: {
1326
+ ...rest,
1327
+ ...await this.#auth.captchaBody(captcha)
1328
+ },
937
1329
  skipAuth: true,
938
1330
  skipAuthRefresh: true,
939
1331
  ...options
@@ -947,20 +1339,25 @@ var AuthResource = class extends require_render.BaseResource {
947
1339
  *
948
1340
  * При успешном входе токен сохраняется в клиенте автоматически.
949
1341
  *
950
- * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
1342
+ * @param credentials email, пароль и токен капчи — см. {@link CredentialsWithCaptcha}
951
1343
  */
952
1344
  async signIn(credentials, options = {}) {
1345
+ const { captcha, ...rest } = credentials;
953
1346
  const revision = this.#auth.revision();
954
1347
  const cookies = this.#auth.createCookieFlow(false);
1348
+ const body = {
1349
+ ...rest,
1350
+ ...await this.#auth.captchaBody(captcha)
1351
+ };
955
1352
  const result = await this.http.execute(AUTH_SIGN_IN, {
956
1353
  path: AUTH_PATHS.signIn,
957
- body: credentials,
1354
+ body,
958
1355
  skipAuth: true,
959
1356
  skipAuthRefresh: true,
960
1357
  cookieJar: cookies,
961
1358
  ...options
962
1359
  });
963
- if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken, revision, cookies);
1360
+ if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
964
1361
  else this.#auth.commitCookieFlow(cookies, revision);
965
1362
  return result;
966
1363
  }
@@ -980,7 +1377,7 @@ var AuthResource = class extends require_render.BaseResource {
980
1377
  cookieJar: cookies,
981
1378
  ...options
982
1379
  });
983
- await this.#auth.setAccessToken(accessToken, revision, cookies);
1380
+ await this.#auth.commitAuthFlow(accessToken, cookies, revision);
984
1381
  return accessToken;
985
1382
  }
986
1383
  /** Отправляет код подтверждения повторно. */
@@ -1082,10 +1479,13 @@ var AuthResource = class extends require_render.BaseResource {
1082
1479
  *
1083
1480
  * @returns `flowToken`, который нужно передать в {@link resetPassword}
1084
1481
  */
1085
- forgotPassword(input, options = {}) {
1482
+ async forgotPassword(input, options = {}) {
1086
1483
  return this.http.execute(AUTH_FORGOT_PASSWORD, {
1087
1484
  path: AUTH_PATHS.forgotPassword,
1088
- body: input,
1485
+ body: {
1486
+ email: input.email,
1487
+ ...await this.#auth.captchaBody(input.captcha)
1488
+ },
1089
1489
  skipAuth: true,
1090
1490
  skipAuthRefresh: true,
1091
1491
  ...options
@@ -1116,23 +1516,21 @@ var AuthResource = class extends require_render.BaseResource {
1116
1516
  * ```ts
1117
1517
  * await itd.auth.resetPasswordWithOtp({
1118
1518
  * email,
1119
- * turnstileToken,
1519
+ * captcha: { type: provider, token, field },
1120
1520
  * newPassword,
1121
1521
  * getOtp: () => rl.question('Код из письма: '),
1122
1522
  * });
1123
1523
  * ```
1124
1524
  */
1125
1525
  async resetPasswordWithOtp(input, options = {}) {
1126
- const flowToken = await this.forgotPassword({
1127
- email: input.email,
1128
- turnstileToken: input.turnstileToken
1129
- }, options);
1130
- const otp = await input.getOtp();
1526
+ const { getOtp, newPassword, ...request } = input;
1527
+ const flowToken = await this.forgotPassword(request, options);
1528
+ const otp = await getOtp();
1131
1529
  await this.resetPassword({
1132
1530
  email: input.email,
1133
1531
  otp,
1134
1532
  flowToken,
1135
- newPassword: input.newPassword
1533
+ newPassword
1136
1534
  }, options);
1137
1535
  }
1138
1536
  /**
@@ -1221,8 +1619,9 @@ function createManagedClient(options, internals) {
1221
1619
  * const itd = new ItdClient({
1222
1620
  * // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
1223
1621
  * // а истёкший продлевается сам. Здесь он нужен на первый запуск.
1224
- * // Вход по паролю требует токена капчи — см. AuthInput и TURNSTILE_SITE_KEY.
1225
- * auth: { email, password, getTurnstileToken },
1622
+ * auth: { email, password },
1623
+ * // Капчу входа решает пакет @itd-api/captcha — см. CaptchaSolver.
1624
+ * captcha: createCaptchaSolver(),
1226
1625
  * storage: new FileTokenStorage('./.itd-session.json'),
1227
1626
  * rateLimit: { concurrency: 4, rps: 8 },
1228
1627
  * });
@@ -1243,7 +1642,10 @@ var ItdClient = class ItdClient {
1243
1642
  #auth;
1244
1643
  /** Авторизация, сессии и пароли. */
1245
1644
  get auth() {
1246
- this.#auth ??= new AuthResource(this.#runtime.http, { auth: this.#runtime.auth });
1645
+ this.#auth ??= new AuthResource(this.#runtime.http, {
1646
+ auth: this.#runtime.auth,
1647
+ connection: this.#runtime.connection()
1648
+ });
1247
1649
  return this.#auth;
1248
1650
  }
1249
1651
  /** Профили, подписки, блокировки, приватность. */
@@ -1733,6 +2135,8 @@ function assertAccountsActive(accounts, action) {
1733
2135
  *
1734
2136
  * await using accounts = new ItdAccounts({
1735
2137
  * storage: new FileMultiTokenStorage('./.itd-sessions.json'),
2138
+ * // Общие опции достаются каждому аккаунту: один источник токена капчи на всех.
2139
+ * captcha: createCaptchaSolver(),
1736
2140
  * rateLimit: { concurrency: 4 },
1737
2141
  * });
1738
2142
  *
@@ -1740,7 +2144,7 @@ function assertAccountsActive(accounts, action) {
1740
2144
  * await accounts.restore();
1741
2145
  *
1742
2146
  * if (!accounts.has('kiow')) {
1743
- * accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
2147
+ * accounts.addAccount('kiow', { auth: { email, password } });
1744
2148
  * }
1745
2149
  *
1746
2150
  * await accounts.account('kiow').posts.create({ content: 'привет' });
@@ -1819,7 +2223,7 @@ var ItdAccounts = class ItdAccounts {
1819
2223
  *
1820
2224
  * @example
1821
2225
  * ```ts
1822
- * accounts.addAccount('bot', { auth: { email, password, getTurnstileToken } });
2226
+ * accounts.addAccount('bot', { auth: { email, password } });
1823
2227
  * accounts.addAccount('reader', { auth: '<accessToken>' });
1824
2228
  * accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
1825
2229
  * ```
@@ -2157,6 +2561,10 @@ exports.AUDIO_MIME_TYPES = require_render.AUDIO_MIME_TYPES;
2157
2561
  exports.AccessType = require_url.AccessType;
2158
2562
  exports.AttachmentType = require_url.AttachmentType;
2159
2563
  exports.BUCKET_LIMITS = require_url.BUCKET_LIMITS;
2564
+ exports.CAPTCHA_FIELDS = CAPTCHA_FIELDS;
2565
+ exports.CaptchaChoice = require_url.CaptchaChoice;
2566
+ exports.CaptchaField = require_url.CaptchaField;
2567
+ exports.CaptchaType = require_url.CaptchaType;
2160
2568
  exports.CommentSort = require_url.CommentSort;
2161
2569
  exports.DEFAULT_BASE_URL = require_url.DEFAULT_BASE_URL;
2162
2570
  exports.DEFAULT_RATE_LIMIT_BUCKET = require_url.DEFAULT_RATE_LIMIT_BUCKET;
@@ -2206,6 +2614,8 @@ exports.NotificationUpdateType = require_events.NotificationUpdateType;
2206
2614
  exports.OPERATIONS = require_url.OPERATIONS;
2207
2615
  exports.PaginationMode = require_render.PaginationMode;
2208
2616
  exports.Paginator = require_render.Paginator;
2617
+ exports.QrLoginStatus = QrLoginStatus;
2618
+ exports.QrLoginStreamStatus = QrLoginStreamStatus;
2209
2619
  exports.RateLimitPacing = require_url.RateLimitPacing;
2210
2620
  exports.ReportReason = require_url.ReportReason;
2211
2621
  exports.ReportTargetType = require_url.ReportTargetType;