itd-api 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +405 -42
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +181 -29
  9. package/dist/index.d.ts +181 -29
  10. package/dist/index.js +401 -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-BzjI6pn5.js → events-B0Wtn4cF.js} +3 -3
  17. package/dist/shared/{events-BzjI6pn5.js.map → events-B0Wtn4cF.js.map} +1 -1
  18. package/dist/shared/{events-C1FudmSe.cjs → events-BB791aPk.cjs} +8 -2
  19. package/dist/shared/{events-C1FudmSe.cjs.map → events-BB791aPk.cjs.map} +1 -1
  20. package/dist/shared/{events-BPxBF0gQ.d.cts → events-BNGjJVI3.d.cts} +2 -2
  21. package/dist/shared/{events-DnwfcLxS.d.ts → events-CJO6q93Z.d.ts} +2 -2
  22. package/dist/shared/{render-Omy1drH1.cjs → render-BimHjOhw.cjs} +2 -2
  23. package/dist/shared/{render-Omy1drH1.cjs.map → render-BimHjOhw.cjs.map} +1 -1
  24. package/dist/shared/{render-C1F_QWYD.d.cts → render-C1A57W-5.d.cts} +2 -2
  25. package/dist/shared/{render-CtOpp9Pm.js → render-CkhvH_yh.js} +2 -2
  26. package/dist/shared/{render-CtOpp9Pm.js.map → render-CkhvH_yh.js.map} +1 -1
  27. package/dist/shared/{render-uFW2kDxp.d.ts → render-DC5nvqcP.d.ts} +2 -2
  28. package/dist/shared/{url-3Ts6EEHQ.js → url-BA5hEwj2.js} +65 -3
  29. package/dist/shared/url-BA5hEwj2.js.map +1 -0
  30. package/dist/shared/{url-D6wsXm1T.cjs → url-CDVgMIq5.cjs} +100 -2
  31. package/dist/shared/url-CDVgMIq5.cjs.map +1 -0
  32. package/dist/shared/{url-DubjkRR9.d.ts → url-DkMueFAT.d.cts} +70 -3
  33. package/dist/shared/{url-DubjkRR9.d.cts → url-DkMueFAT.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-CDVgMIq5.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-BimHjOhw.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-BB791aPk.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,84 @@ 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
+ * Проверяет форму блока `auth.captcha`.
96
+ *
97
+ * Отсутствие источника токена ошибкой не считается: до входа по паролю дело может и не
98
+ * дойти. О нехватке сообщит {@link resolveCaptchaBody} в момент входа.
99
+ *
100
+ * @throws {ItdConfigError} при некорректных значениях
101
+ */
102
+ function validateCaptchaOptions(captcha) {
103
+ if (captcha === void 0) return;
104
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("auth.captcha должен быть объектом");
105
+ const { type, token, getToken, field } = captcha;
106
+ requireCaptchaText(type, "auth.captcha.type");
107
+ requireCaptchaText(token, "auth.captcha.token");
108
+ requireCaptchaText(field, "auth.captcha.field");
109
+ if (getToken !== void 0 && typeof getToken !== "function") throw new require_errors.ItdConfigError("auth.captcha.getToken должен быть функцией");
110
+ }
111
+ /** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
112
+ function readCaptchaProvider(body) {
113
+ const config = require_url.isRecord$1(body) ? body : void 0;
114
+ const provider = config?.provider;
115
+ const field = config?.field;
116
+ if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new require_errors.ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
117
+ return {
118
+ provider,
119
+ field
120
+ };
121
+ }
122
+ /** Куда класть токен: настройка, иначе умолчание провайдера. */
123
+ function resolveField(type, configured) {
124
+ const field = configured ?? CAPTCHA_FIELDS[type];
125
+ if (field) return field;
126
+ throw new require_errors.ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте auth.captcha.field либо оставьте auth.captcha.type = ${require_url.CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
127
+ }
128
+ /** Спрашивает свежий токен, затем берёт разовый. */
129
+ async function resolveToken(captcha, type) {
130
+ const token = await captcha.getToken?.(type) || captcha.token;
131
+ if (token) return token;
132
+ throw new require_errors.ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте auth.captcha.getToken.`);
133
+ }
134
+ /**
135
+ * Готовит фрагмент тела запроса с токеном капчи.
136
+ *
137
+ * При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
138
+ * и имя поля приходят оттуда. Явно названный тип решается без этого запроса.
139
+ *
140
+ * @param askProvider как спросить активного провайдера; вызывается, только если это нужно
141
+ * @throws {ItdConfigError} если капча не настроена или токен не получен
142
+ */
143
+ async function resolveCaptchaBody(captcha, askProvider) {
144
+ if (!captcha?.token && !captcha?.getToken) throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи. Передайте auth.captcha.getToken — источник свежего токена — либо разовый auth.captcha.token. В Node токен получает пакет @itd-api/captcha.");
145
+ const choice = captcha.type ?? require_url.CaptchaChoice.Auto;
146
+ if (choice === require_url.CaptchaChoice.Auto) {
147
+ const { provider, field } = await askProvider();
148
+ return { [field]: await resolveToken(captcha, provider) };
149
+ }
150
+ return { [resolveField(choice, captcha.field)]: await resolveToken(captcha, choice) };
151
+ }
152
+ /**
153
+ * Готовит фрагмент тела для разового вызова с готовым доказательством.
154
+ *
155
+ * Поле берётся из самого доказательства либо из умолчания провайдера; запроса к серверу нет.
156
+ *
157
+ * @throws {ItdConfigError} если доказательство неполное
158
+ */
159
+ function captchaBody(captcha) {
160
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("captcha должен быть объектом { type, token }");
161
+ if (typeof captcha.token !== "string" || captcha.token.trim() === "") throw new require_errors.ItdConfigError("captcha.token должен быть непустой строкой");
162
+ if (typeof captcha.type !== "string" || captcha.type.trim() === "") throw new require_errors.ItdConfigError("captcha.type должен быть непустой строкой");
163
+ return { [resolveField(captcha.type, captcha.field)]: captcha.token };
164
+ }
62
165
  //#endregion
63
166
  //#region src/session/jwt.ts
64
167
  /**
@@ -131,11 +234,10 @@ function validateAuth(auth) {
131
234
  return { ...auth };
132
235
  }
133
236
  if ("email" in auth || "password" in auth) {
134
- const { email, password, turnstileToken, getTurnstileToken } = auth;
237
+ const { email, password, captcha } = auth;
135
238
  if (typeof email !== "string" || email.trim() === "") throw new require_errors.ItdConfigError("auth.email должен быть непустой строкой");
136
239
  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 должен быть непустой строкой");
240
+ validateCaptchaOptions(captcha);
139
241
  return { ...auth };
140
242
  }
141
243
  throw new require_errors.ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
@@ -182,6 +284,7 @@ function createItdAuth(options, deps) {
182
284
  }
183
285
  /** Пути эндпоинтов авторизации. */
184
286
  const AUTH_PATHS = {
287
+ captchaProvider: "/api/v1/auth/captcha/provider",
185
288
  signUp: "/api/v1/auth/sign-up",
186
289
  signIn: "/api/v1/auth/sign-in",
187
290
  verifyOtp: "/api/v1/auth/verify-otp",
@@ -191,7 +294,10 @@ const AUTH_PATHS = {
191
294
  forgotPassword: "/api/v1/auth/forgot-password",
192
295
  resetPassword: "/api/v1/auth/reset-password",
193
296
  changePassword: "/api/v1/auth/change-password",
194
- sessions: "/api/v1/auth/sessions"
297
+ sessions: "/api/v1/auth/sessions",
298
+ qrStart: "/api/v1/auth/qr/start",
299
+ qrClaim: "/api/v1/auth/qr/claim",
300
+ qrStream: "/api/v1/auth/qr/stream"
195
301
  };
196
302
  /**
197
303
  * Публичный ключ Cloudflare Turnstile платформы итд.com.
@@ -201,14 +307,15 @@ const AUTH_PATHS = {
201
307
  *
202
308
  * Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
203
309
  * Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
204
- * подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
310
+ * подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
205
311
  * без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
206
312
  *
207
313
  * @example
208
314
  * ```ts
209
315
  * turnstile.render('#captcha', {
210
316
  * sitekey: TURNSTILE_SITE_KEY,
211
- * callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
317
+ * callback: (token) =>
318
+ * itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
212
319
  * });
213
320
  * ```
214
321
  */
@@ -626,6 +733,13 @@ var AuthManager = class {
626
733
  }
627
734
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
628
735
  async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
736
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
737
+ }
738
+ /** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
739
+ async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
740
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
741
+ }
742
+ async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
629
743
  this.#assertActive();
630
744
  if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
631
745
  await this.#loadSession();
@@ -635,7 +749,7 @@ var AuthManager = class {
635
749
  this.#invalidateInFlight();
636
750
  this.#transitionAuth(accessToken);
637
751
  const saved = this.#saveSession({
638
- ...this.#session ?? {},
752
+ ...replaceSession ? {} : this.#session ?? {},
639
753
  accessToken,
640
754
  obtainedAt: this.#config.clock.now()
641
755
  });
@@ -850,29 +964,23 @@ var AuthManager = class {
850
964
  this.#signingIn = promise;
851
965
  return promise;
852
966
  }
853
- /**
854
- * Берёт токен капчи для входа.
855
- *
856
- * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
857
- * несколько минут, поэтому при повторном входе через сутки годится только свежий.
858
- */
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().");
967
+ /** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
968
+ async #resolveCaptchaProvider() {
969
+ return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
970
+ path: AUTH_PATHS.captchaProvider,
971
+ skipAuth: true,
972
+ skipAuthRefresh: true
973
+ }));
866
974
  }
867
975
  async #performSignIn(credentials, revision) {
868
- const turnstileToken = await this.#resolveTurnstileToken(credentials);
976
+ const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
869
977
  const flowCookies = new require_redact.CookieJar();
870
978
  const result = await this.#http.execute(AUTH_SIGN_IN, {
871
979
  path: AUTH_PATHS.signIn,
872
980
  body: {
873
981
  email: credentials.email,
874
982
  password: credentials.password,
875
- turnstileToken
983
+ ...captcha
876
984
  },
877
985
  skipAuth: true,
878
986
  skipAuthRefresh: true,
@@ -896,6 +1004,125 @@ var AuthManager = class {
896
1004
  }
897
1005
  };
898
1006
  //#endregion
1007
+ //#region src/session/qr-stream.ts
1008
+ const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
1009
+ function readQrLoginStreamEvent(data) {
1010
+ let value;
1011
+ try {
1012
+ value = JSON.parse(data);
1013
+ } catch {
1014
+ return;
1015
+ }
1016
+ if (!require_url.isRecord$1(value)) return void 0;
1017
+ const event = value;
1018
+ if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
1019
+ return {
1020
+ status: event.status,
1021
+ ...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
1022
+ };
1023
+ }
1024
+ /** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
1025
+ async function consumeQrLoginStream(body, onEvent, signal) {
1026
+ const reader = body.getReader();
1027
+ const decoder = new TextDecoder();
1028
+ let delivery = Promise.resolve();
1029
+ let completed = false;
1030
+ const cancel = () => {
1031
+ reader.cancel(signal?.reason).catch(() => {});
1032
+ };
1033
+ signal?.addEventListener("abort", cancel, { once: true });
1034
+ const parser = require_events.createParser({ onEvent(message) {
1035
+ const event = readQrLoginStreamEvent(message.data);
1036
+ if (!event) return;
1037
+ delivery = delivery.then(() => onEvent(event));
1038
+ } });
1039
+ try {
1040
+ for (;;) {
1041
+ const { done, value } = await reader.read();
1042
+ if (done) break;
1043
+ parser.feed(decoder.decode(value, { stream: true }));
1044
+ await delivery;
1045
+ }
1046
+ parser.feed(decoder.decode());
1047
+ await delivery;
1048
+ if (signal?.aborted) throw signal.reason;
1049
+ completed = true;
1050
+ } finally {
1051
+ signal?.removeEventListener("abort", cancel);
1052
+ if (!completed) await reader.cancel().catch(() => {});
1053
+ reader.releaseLock?.();
1054
+ }
1055
+ }
1056
+ /** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
1057
+ async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
1058
+ connection.assertActive?.("открыть QR-поток");
1059
+ const path = AUTH_PATHS.qrStream;
1060
+ const method = "POST";
1061
+ const url = require_redact.joinUrl(connection.baseUrl, path);
1062
+ const abort = require_url.createRequestAbortScope(signal, connection.signal, 0, connection.clock);
1063
+ try {
1064
+ const headers = await connection.baseHeaders(url);
1065
+ headers.delete("Authorization");
1066
+ headers.delete("Cookie");
1067
+ headers.set("Accept", "text/event-stream");
1068
+ headers.set("Content-Type", "application/json");
1069
+ const cookie = cookies.getHeader(url);
1070
+ if (cookie) headers.set("Cookie", cookie);
1071
+ let response;
1072
+ try {
1073
+ response = await connection.fetch(url, {
1074
+ method,
1075
+ headers,
1076
+ body: JSON.stringify(input),
1077
+ credentials: "include",
1078
+ signal: abort.signal
1079
+ });
1080
+ } catch (error) {
1081
+ const failure = new require_errors.ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
1082
+ method,
1083
+ path,
1084
+ cause: error
1085
+ });
1086
+ throw require_url.requestAbortError(abort, {
1087
+ timeout: 0,
1088
+ method,
1089
+ path
1090
+ }, failure);
1091
+ }
1092
+ cookies.setFromResponse(response.url || url, response);
1093
+ if (!response.ok) {
1094
+ const text = await response.text();
1095
+ let body = text;
1096
+ try {
1097
+ body = JSON.parse(text);
1098
+ } catch {}
1099
+ throw require_url.createApiError({
1100
+ method,
1101
+ path,
1102
+ status: response.status,
1103
+ statusText: response.statusText,
1104
+ headers: response.headers,
1105
+ response,
1106
+ body,
1107
+ now: connection.clock.now()
1108
+ });
1109
+ }
1110
+ if (!response.body) throw new require_errors.ItdNetworkError("Сервер не вернул тело QR-потока", {
1111
+ method,
1112
+ path
1113
+ });
1114
+ await consumeQrLoginStream(response.body, onEvent, abort.signal);
1115
+ } catch (error) {
1116
+ throw require_url.requestAbortError(abort, {
1117
+ timeout: 0,
1118
+ method,
1119
+ path
1120
+ }, error);
1121
+ } finally {
1122
+ abort.cleanup();
1123
+ }
1124
+ }
1125
+ //#endregion
899
1126
  //#region src/resources/auth.ts
900
1127
  /**
901
1128
  * Результат входа.
@@ -910,9 +1137,35 @@ var AuthManager = class {
910
1137
  */
911
1138
  var AuthResource = class extends require_render.BaseResource {
912
1139
  #auth;
1140
+ #connection;
1141
+ #qrFlows = /* @__PURE__ */ new Map();
913
1142
  constructor(http, deps) {
914
1143
  super(http);
915
1144
  this.#auth = deps.auth;
1145
+ this.#connection = deps.connection;
1146
+ }
1147
+ #pruneQrFlows() {
1148
+ const now = this.#connection.clock.now();
1149
+ for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
1150
+ }
1151
+ #qrFlow(qrId) {
1152
+ this.#pruneQrFlows();
1153
+ const existing = this.#qrFlows.get(qrId);
1154
+ if (existing) return existing;
1155
+ const flow = {
1156
+ cookies: this.#auth.createCookieFlow(false),
1157
+ expiresAt: void 0
1158
+ };
1159
+ this.#qrFlows.set(qrId, flow);
1160
+ return flow;
1161
+ }
1162
+ #touchQrFlow(flow, expiresIn) {
1163
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
1164
+ const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
1165
+ flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
1166
+ }
1167
+ #forgetTerminalQrError(qrId, error) {
1168
+ if (require_errors.isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
916
1169
  }
917
1170
  /**
918
1171
  * Проверяет состояние авторизации и возвращает текущего пользователя.
@@ -926,14 +1179,106 @@ var AuthResource = class extends require_render.BaseResource {
926
1179
  });
927
1180
  }
928
1181
  /**
1182
+ * Узнаёт активного провайдера капчи и поле токена.
1183
+ *
1184
+ * Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
1185
+ * Автоматическому входу этот вызов не нужен — клиент делает его сам.
1186
+ */
1187
+ captchaProvider(options = {}) {
1188
+ return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
1189
+ path: AUTH_PATHS.captchaProvider,
1190
+ skipAuth: true,
1191
+ skipAuthRefresh: true,
1192
+ ...options
1193
+ });
1194
+ }
1195
+ /** Создаёт короткоживущую сессию QR-входа. */
1196
+ async startQrLogin(options = {}) {
1197
+ const cookies = this.#auth.createCookieFlow(false);
1198
+ const result = await this.http.execute(AUTH_QR_START, {
1199
+ path: AUTH_PATHS.qrStart,
1200
+ skipAuth: true,
1201
+ skipAuthRefresh: true,
1202
+ cookieJar: cookies,
1203
+ ...options
1204
+ });
1205
+ if (typeof result.qrId === "string" && result.qrId !== "") {
1206
+ const flow = {
1207
+ cookies,
1208
+ expiresAt: void 0
1209
+ };
1210
+ this.#touchQrFlow(flow, result.expiresIn);
1211
+ this.#qrFlows.set(result.qrId, flow);
1212
+ }
1213
+ return result;
1214
+ }
1215
+ /**
1216
+ * Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
1217
+ *
1218
+ * До подтверждения возвращает промежуточный статус. Если сервер потребовал капчу,
1219
+ * повторите вызов с `captcha` — тип берётся из {@link captchaProvider}.
1220
+ * Полученный `accessToken` автоматически сохраняется в клиенте.
1221
+ */
1222
+ async claimQrLogin(input, options = {}) {
1223
+ const revision = this.#auth.revision();
1224
+ const flow = this.#qrFlow(input.qrId);
1225
+ try {
1226
+ const { captcha, ...rest } = input;
1227
+ const result = await this.http.execute(AUTH_QR_CLAIM, {
1228
+ path: AUTH_PATHS.qrClaim,
1229
+ body: {
1230
+ ...rest,
1231
+ ...captcha ? captchaBody(captcha) : {}
1232
+ },
1233
+ skipAuth: true,
1234
+ skipAuthRefresh: true,
1235
+ cookieJar: flow.cookies,
1236
+ ...options
1237
+ });
1238
+ this.#touchQrFlow(flow, result.expiresIn);
1239
+ if (result.status === QrLoginStatus.Authorized) {
1240
+ await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
1241
+ this.#qrFlows.delete(input.qrId);
1242
+ } else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
1243
+ return result;
1244
+ } catch (error) {
1245
+ this.#forgetTerminalQrError(input.qrId, error);
1246
+ throw error;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
1251
+ *
1252
+ * Событие `approved` означает, что подтверждение завершено: после него вызовите
1253
+ * {@link claimQrLogin}, чтобы получить и сохранить access token.
1254
+ */
1255
+ async streamQrLogin(input, onEvent, options = {}) {
1256
+ if (typeof onEvent !== "function") throw new require_errors.ItdConfigError("onEvent QR-потока должен быть функцией");
1257
+ const flow = this.#qrFlow(input.qrId);
1258
+ try {
1259
+ await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
1260
+ this.#touchQrFlow(flow, event.expiresIn);
1261
+ if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
1262
+ await onEvent(event);
1263
+ }, options.signal);
1264
+ } catch (error) {
1265
+ this.#forgetTerminalQrError(input.qrId, error);
1266
+ throw error;
1267
+ }
1268
+ }
1269
+ /**
929
1270
  * Регистрирует аккаунт и запускает подтверждение по коду.
930
1271
  *
931
1272
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
932
1273
  */
933
1274
  signUp(credentials, options = {}) {
1275
+ const { captcha, ...rest } = credentials;
934
1276
  return this.http.execute(AUTH_SIGN_UP, {
935
1277
  path: AUTH_PATHS.signUp,
936
- body: credentials,
1278
+ body: {
1279
+ ...rest,
1280
+ ...captchaBody(captcha)
1281
+ },
937
1282
  skipAuth: true,
938
1283
  skipAuthRefresh: true,
939
1284
  ...options
@@ -950,17 +1295,21 @@ var AuthResource = class extends require_render.BaseResource {
950
1295
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
951
1296
  */
952
1297
  async signIn(credentials, options = {}) {
1298
+ const { captcha, ...rest } = credentials;
953
1299
  const revision = this.#auth.revision();
954
1300
  const cookies = this.#auth.createCookieFlow(false);
955
1301
  const result = await this.http.execute(AUTH_SIGN_IN, {
956
1302
  path: AUTH_PATHS.signIn,
957
- body: credentials,
1303
+ body: {
1304
+ ...rest,
1305
+ ...captchaBody(captcha)
1306
+ },
958
1307
  skipAuth: true,
959
1308
  skipAuthRefresh: true,
960
1309
  cookieJar: cookies,
961
1310
  ...options
962
1311
  });
963
- if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken, revision, cookies);
1312
+ if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
964
1313
  else this.#auth.commitCookieFlow(cookies, revision);
965
1314
  return result;
966
1315
  }
@@ -980,7 +1329,7 @@ var AuthResource = class extends require_render.BaseResource {
980
1329
  cookieJar: cookies,
981
1330
  ...options
982
1331
  });
983
- await this.#auth.setAccessToken(accessToken, revision, cookies);
1332
+ await this.#auth.commitAuthFlow(accessToken, cookies, revision);
984
1333
  return accessToken;
985
1334
  }
986
1335
  /** Отправляет код подтверждения повторно. */
@@ -1085,7 +1434,10 @@ var AuthResource = class extends require_render.BaseResource {
1085
1434
  forgotPassword(input, options = {}) {
1086
1435
  return this.http.execute(AUTH_FORGOT_PASSWORD, {
1087
1436
  path: AUTH_PATHS.forgotPassword,
1088
- body: input,
1437
+ body: {
1438
+ email: input.email,
1439
+ ...captchaBody(input.captcha)
1440
+ },
1089
1441
  skipAuth: true,
1090
1442
  skipAuthRefresh: true,
1091
1443
  ...options
@@ -1116,23 +1468,21 @@ var AuthResource = class extends require_render.BaseResource {
1116
1468
  * ```ts
1117
1469
  * await itd.auth.resetPasswordWithOtp({
1118
1470
  * email,
1119
- * turnstileToken,
1471
+ * captcha: { type: provider, token, field },
1120
1472
  * newPassword,
1121
1473
  * getOtp: () => rl.question('Код из письма: '),
1122
1474
  * });
1123
1475
  * ```
1124
1476
  */
1125
1477
  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();
1478
+ const { getOtp, newPassword, ...request } = input;
1479
+ const flowToken = await this.forgotPassword(request, options);
1480
+ const otp = await getOtp();
1131
1481
  await this.resetPassword({
1132
1482
  email: input.email,
1133
1483
  otp,
1134
1484
  flowToken,
1135
- newPassword: input.newPassword
1485
+ newPassword
1136
1486
  }, options);
1137
1487
  }
1138
1488
  /**
@@ -1221,8 +1571,8 @@ function createManagedClient(options, internals) {
1221
1571
  * const itd = new ItdClient({
1222
1572
  * // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
1223
1573
  * // а истёкший продлевается сам. Здесь он нужен на первый запуск.
1224
- * // Вход по паролю требует токена капчи — см. AuthInput и TURNSTILE_SITE_KEY.
1225
- * auth: { email, password, getTurnstileToken },
1574
+ * // Вход по паролю требует токена капчи — см. CaptchaOptions.
1575
+ * auth: { email, password, captcha: createCaptchaSolver() },
1226
1576
  * storage: new FileTokenStorage('./.itd-session.json'),
1227
1577
  * rateLimit: { concurrency: 4, rps: 8 },
1228
1578
  * });
@@ -1243,7 +1593,10 @@ var ItdClient = class ItdClient {
1243
1593
  #auth;
1244
1594
  /** Авторизация, сессии и пароли. */
1245
1595
  get auth() {
1246
- this.#auth ??= new AuthResource(this.#runtime.http, { auth: this.#runtime.auth });
1596
+ this.#auth ??= new AuthResource(this.#runtime.http, {
1597
+ auth: this.#runtime.auth,
1598
+ connection: this.#runtime.connection()
1599
+ });
1247
1600
  return this.#auth;
1248
1601
  }
1249
1602
  /** Профили, подписки, блокировки, приватность. */
@@ -1740,7 +2093,9 @@ function assertAccountsActive(accounts, action) {
1740
2093
  * await accounts.restore();
1741
2094
  *
1742
2095
  * if (!accounts.has('kiow')) {
1743
- * accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
2096
+ * accounts.addAccount('kiow', {
2097
+ * auth: { email, password, captcha: createCaptchaSolver() },
2098
+ * });
1744
2099
  * }
1745
2100
  *
1746
2101
  * await accounts.account('kiow').posts.create({ content: 'привет' });
@@ -1819,7 +2174,9 @@ var ItdAccounts = class ItdAccounts {
1819
2174
  *
1820
2175
  * @example
1821
2176
  * ```ts
1822
- * accounts.addAccount('bot', { auth: { email, password, getTurnstileToken } });
2177
+ * accounts.addAccount('bot', {
2178
+ * auth: { email, password, captcha: createCaptchaSolver() },
2179
+ * });
1823
2180
  * accounts.addAccount('reader', { auth: '<accessToken>' });
1824
2181
  * accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
1825
2182
  * ```
@@ -2157,6 +2514,10 @@ exports.AUDIO_MIME_TYPES = require_render.AUDIO_MIME_TYPES;
2157
2514
  exports.AccessType = require_url.AccessType;
2158
2515
  exports.AttachmentType = require_url.AttachmentType;
2159
2516
  exports.BUCKET_LIMITS = require_url.BUCKET_LIMITS;
2517
+ exports.CAPTCHA_FIELDS = CAPTCHA_FIELDS;
2518
+ exports.CaptchaChoice = require_url.CaptchaChoice;
2519
+ exports.CaptchaField = require_url.CaptchaField;
2520
+ exports.CaptchaType = require_url.CaptchaType;
2160
2521
  exports.CommentSort = require_url.CommentSort;
2161
2522
  exports.DEFAULT_BASE_URL = require_url.DEFAULT_BASE_URL;
2162
2523
  exports.DEFAULT_RATE_LIMIT_BUCKET = require_url.DEFAULT_RATE_LIMIT_BUCKET;
@@ -2206,6 +2567,8 @@ exports.NotificationUpdateType = require_events.NotificationUpdateType;
2206
2567
  exports.OPERATIONS = require_url.OPERATIONS;
2207
2568
  exports.PaginationMode = require_render.PaginationMode;
2208
2569
  exports.Paginator = require_render.Paginator;
2570
+ exports.QrLoginStatus = QrLoginStatus;
2571
+ exports.QrLoginStreamStatus = QrLoginStreamStatus;
2209
2572
  exports.RateLimitPacing = require_url.RateLimitPacing;
2210
2573
  exports.ReportReason = require_url.ReportReason;
2211
2574
  exports.ReportTargetType = require_url.ReportTargetType;