itd-api 0.9.2 → 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 (38) 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 +489 -92
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +186 -36
  9. package/dist/index.d.ts +186 -36
  10. package/dist/index.js +485 -94
  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-BIBO2Jif.js → events-B0Wtn4cF.js} +3 -3
  17. package/dist/shared/{events-BIBO2Jif.js.map → events-B0Wtn4cF.js.map} +1 -1
  18. package/dist/shared/{events-BsRm_Fba.cjs → events-BB791aPk.cjs} +8 -2
  19. package/dist/shared/{events-BsRm_Fba.cjs.map → events-BB791aPk.cjs.map} +1 -1
  20. package/dist/shared/{events-BCn5LRnT.d.cts → events-BNGjJVI3.d.cts} +2 -2
  21. package/dist/shared/{events-t1uGgpkf.d.ts → events-CJO6q93Z.d.ts} +2 -2
  22. package/dist/shared/{render-DqVzx68r.cjs → render-BimHjOhw.cjs} +2 -30
  23. package/dist/shared/render-BimHjOhw.cjs.map +1 -0
  24. package/dist/shared/{render-DA83gS0f.d.cts → render-C1A57W-5.d.cts} +2 -14
  25. package/dist/shared/{render--ZXmYEf-.js → render-CkhvH_yh.js} +2 -30
  26. package/dist/shared/render-CkhvH_yh.js.map +1 -0
  27. package/dist/shared/{render-BdnBV-aT.d.ts → render-DC5nvqcP.d.ts} +2 -14
  28. package/dist/shared/{url-i2f-X1hX.js → url-BA5hEwj2.js} +95 -16
  29. package/dist/shared/url-BA5hEwj2.js.map +1 -0
  30. package/dist/shared/{url-Doc0XXW_.cjs → url-CDVgMIq5.cjs} +130 -15
  31. package/dist/shared/url-CDVgMIq5.cjs.map +1 -0
  32. package/dist/shared/{url-Dbj6V6fk.d.cts → url-DkMueFAT.d.cts} +76 -17
  33. package/dist/shared/{url-Dbj6V6fk.d.ts → url-DkMueFAT.d.ts} +76 -17
  34. package/package.json +4 -3
  35. package/dist/shared/render--ZXmYEf-.js.map +0 -1
  36. package/dist/shared/render-DqVzx68r.cjs.map +0 -1
  37. package/dist/shared/url-Doc0XXW_.cjs.map +0 -1
  38. package/dist/shared/url-i2f-X1hX.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as orderPluginDefinitions, A as ITD_CATALOG, C as ReportReason, D as ViewReason, E as SpanType, F as operationMethod, G as pickString, I as operationRetrySafety, K as RetrySafety, L as BUCKET_LIMITS, M as defineBuiltInOperation, N as isBuiltInOperationId, O as ViewSource, P as operationBucket, Q as assertPluginRemovable, R as DEFAULT_RATE_LIMIT_BUCKET, S as NotificationType, T as ServiceState, V as pickArray, Z as RequestQueuePool, _ as FeedTab, at as isRecord, b as ItdErrorCode, c as normalizeNotification, d as canonicalNotificationType, et as DEFAULT_BASE_URL, f as isKnownNotificationType, g as EventChannelStatus, h as CommentSort, it as LIBRARY_VERSION, j as OPERATIONS, k as WallAccess, lt as systemClock, m as AttachmentType, n as formatNotificationText, nt as STATUS_SERVICE, o as passthroughOperation, ot as requireOptionalBoolean, p as AccessType, rt as resolveRateLimit, s as voidOperation, st as RateLimitPacing, t as resolveNotificationUrl, ut as installAsyncDisposeFallback, v as IncidentKind, w as ReportTargetType, x as LikesVisibility, y as InteractionType, z as createClientRuntime } from "./shared/url-i2f-X1hX.js";
1
+ import { A as ViewReason, B as BUCKET_LIMITS, C as ItdErrorCode, D as ReportTargetType, E as ReportReason, F as defineBuiltInOperation, G as pickArray, H as createClientRuntime, I as isBuiltInOperationId, L as operationBucket, M as WallAccess, N as ITD_CATALOG, O as ServiceState, P as OPERATIONS, R as operationMethod, S as InteractionType, T as NotificationType, U as createApiError, V as DEFAULT_RATE_LIMIT_BUCKET, X as RetrySafety, Y as pickString, _ as CaptchaType, at as orderPluginDefinitions, b as FeedTab, c as normalizeNotification, ct as STATUS_SERVICE, d as canonicalNotificationType, dt as isRecord, et as createRequestAbortScope, f as isKnownNotificationType, ft as requireOptionalBoolean, g as CaptchaField, gt as installAsyncDisposeFallback, h as CaptchaChoice, ht as systemClock, it as assertPluginRemovable, j as ViewSource, k as SpanType, lt as resolveRateLimit, m as AttachmentType, n as formatNotificationText, o as passthroughOperation, ot as DEFAULT_BASE_URL, p as AccessType, pt as RateLimitPacing, rt as RequestQueuePool, s as voidOperation, t as resolveNotificationUrl, tt as requestAbortError, ut as LIBRARY_VERSION, v as CommentSort, w as LikesVisibility, x as IncidentKind, y as EventChannelStatus, z as operationRetrySafety } from "./shared/url-BA5hEwj2.js";
2
2
  import { A as isItdStateError, C as isItdError, D as isItdPhoneVerificationError, E as isItdNotFoundError, O as isItdRateLimitError, S as isItdConflictError, T as isItdForbiddenError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, c as ItdErrorKind, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, j as isItdValidationError, k as isItdServerError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, r as ItdApiErrorKind, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason, v as ItdTimeoutError, w as isItdFileError, x as isItdAuthError, y as ItdValidationError } from "./shared/errors-GI10kZxk.js";
3
- import { c as REFRESH_COOKIE, l as REFRESH_COOKIE_PATH, s as CookieJar, u as isSerializedCookieEntry, v as RuntimeMode, y as createDeviceId } from "./shared/redact-DJV0XM3n.js";
4
- import { A as utcStampToIso, C as AUDIO_MIME_TYPES, D as isBuilder, E as comment, F as mapPage, I as createFeatureHost, L as ExtensibleOperationCatalog, M as BaseResource, N as PaginationMode, O as createStatusFeature, P as Paginator, S as ALLOWED_MIME_TYPES, T as VIDEO_MIME_TYPES, _ as poll, a as createShopOrderAccessStorage, b as parseHtml, c as ShopOrderStatus, d as isMyProfile, f as fromStream, g as post, h as report, i as MemoryShopOrderAccessStorage, j as NotificationsResource, k as toDate, l as ShopProductCategory, m as createResources, n as renderSpans, o as statusDays, p as fromUrl, r as createShopFeature, s as ShopConsentKind, t as SpanRenderFormat, u as ShopProductStatus, v as autoSpans, w as IMAGE_MIME_TYPES, x as parseMarkdown, y as markup } from "./shared/render--ZXmYEf-.js";
3
+ import { c as REFRESH_COOKIE, h as joinUrl, l as REFRESH_COOKIE_PATH, s as CookieJar, u as isSerializedCookieEntry, v as RuntimeMode, y as createDeviceId } from "./shared/redact-DJV0XM3n.js";
4
+ import { A as utcStampToIso, C as AUDIO_MIME_TYPES, D as isBuilder, E as comment, F as mapPage, I as createFeatureHost, L as ExtensibleOperationCatalog, M as BaseResource, N as PaginationMode, O as createStatusFeature, P as Paginator, S as ALLOWED_MIME_TYPES, T as VIDEO_MIME_TYPES, _ as poll, a as createShopOrderAccessStorage, b as parseHtml, c as ShopOrderStatus, d as isMyProfile, f as fromStream, g as post, h as report, i as MemoryShopOrderAccessStorage, j as NotificationsResource, k as toDate, l as ShopProductCategory, m as createResources, n as renderSpans, o as statusDays, p as fromUrl, r as createShopFeature, s as ShopConsentKind, t as SpanRenderFormat, u as ShopProductStatus, v as autoSpans, w as IMAGE_MIME_TYPES, x as parseMarkdown, y as markup } from "./shared/render-CkhvH_yh.js";
5
5
  import { c as FileTransferMode } from "./shared/options-i4LrKfqx.js";
6
- import { _ as reportListenerError, a as WebSocketTransport, c as NotificationEventsTransport, d as NotificationUpdateOrigin, f as NotificationUpdateType, g as Emitter, h as runEventMiddleware, i as createNotificationEventsClient, l as resolveNotificationEventsOptions, m as UnauthorizedStreamError, n as EventRouter, o as EVENT_OPERATIONS, p as EventChannel, r as NotificationEventsClient, s as NotificationEvents, t as EventComposer, u as setNotificationEventsConnectGuard } from "./shared/events-BIBO2Jif.js";
6
+ import { _ as Emitter, a as WebSocketTransport, c as NotificationEventsTransport, d as NotificationUpdateOrigin, f as NotificationUpdateType, g as runEventMiddleware, h as UnauthorizedStreamError, i as createNotificationEventsClient, l as resolveNotificationEventsOptions, m as EventChannel, n as EventRouter, o as EVENT_OPERATIONS, p as createParser, r as NotificationEventsClient, s as NotificationEvents, t as EventComposer, u as setNotificationEventsConnectGuard, v as reportListenerError } from "./shared/events-B0Wtn4cF.js";
7
7
  import { a as isEnumerableKeyValueStore, i as createRecordKeyValueStore, o as withCodec, r as createKeyValueStore, s as withNamespace, t as MemoryKeyValueStore } from "./shared/key-value-store-Bt2PSMxY.js";
8
8
  import { n as copySession, r as createTokenStorage, t as MemoryTokenStorage } from "./shared/storage-DiDe0PZM.js";
9
9
  import { a as scopedTokenStorage, i as isRestorableSession, n as controlledTokenStorage, r as createMultiTokenStorage, t as MemoryMultiTokenStorage } from "./shared/multi-storage-C3Ngtop1.js";
@@ -31,12 +31,37 @@ const SignInStatus = Object.freeze({
31
31
  Authenticated: "authenticated",
32
32
  OtpRequired: "otp_required"
33
33
  });
34
+ /**
35
+ * Поле тела запроса по умолчанию для известных провайдеров.
36
+ *
37
+ * Используется, когда провайдера не спрашивали. Ответ `captchaProvider()` важнее этой таблицы.
38
+ */
39
+ const CAPTCHA_FIELDS = Object.freeze({
40
+ [CaptchaType.Itd]: CaptchaField.Itd,
41
+ [CaptchaType.Cloudflare]: CaptchaField.Cloudflare
42
+ });
43
+ /** Состояния, которые может вернуть проверка QR-входа. */
44
+ const QrLoginStatus = Object.freeze({
45
+ Pending: "pending",
46
+ Scanned: "scanned",
47
+ CaptchaRequired: "captcha_required",
48
+ Authorized: "authorized",
49
+ Rejected: "rejected"
50
+ });
51
+ /** Состояния, приходящие из потокового наблюдения за QR-входом. */
52
+ const QrLoginStreamStatus = Object.freeze({
53
+ Pending: "pending",
54
+ Scanned: "scanned",
55
+ Approved: "approved",
56
+ Rejected: "rejected"
57
+ });
34
58
  function requireString(body, key, message) {
35
59
  const value = pickString(body, key);
36
60
  if (!value) throw new ItdConfigError(message);
37
61
  return value;
38
62
  }
39
63
  const AUTH_CHECK = passthroughOperation("auth.check");
64
+ const AUTH_CAPTCHA_PROVIDER = passthroughOperation("auth.captchaProvider");
40
65
  const AUTH_SIGN_UP = defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
41
66
  const AUTH_SIGN_IN = defineBuiltInOperation("auth.signIn", (body) => {
42
67
  const accessToken = pickString(body, "accessToken");
@@ -58,6 +83,84 @@ const AUTH_CHANGE_PASSWORD = voidOperation("auth.changePassword");
58
83
  const AUTH_SESSIONS = defineBuiltInOperation("auth.sessions", (body) => pickArray(body, "sessions"));
59
84
  const AUTH_REVOKE_SESSION = voidOperation("auth.revokeSession");
60
85
  const AUTH_REVOKE_OTHER_SESSIONS = voidOperation("auth.revokeOtherSessions");
86
+ const AUTH_QR_START = passthroughOperation("auth.qrStart");
87
+ const AUTH_QR_CLAIM = passthroughOperation("auth.qrClaim");
88
+ //#endregion
89
+ //#region src/session/captcha.ts
90
+ function requireCaptchaText(value, field) {
91
+ if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new ItdConfigError(`${field} должен быть непустой строкой`);
92
+ }
93
+ /**
94
+ * Проверяет форму блока `auth.captcha`.
95
+ *
96
+ * Отсутствие источника токена ошибкой не считается: до входа по паролю дело может и не
97
+ * дойти. О нехватке сообщит {@link resolveCaptchaBody} в момент входа.
98
+ *
99
+ * @throws {ItdConfigError} при некорректных значениях
100
+ */
101
+ function validateCaptchaOptions(captcha) {
102
+ if (captcha === void 0) return;
103
+ if (!isRecord(captcha)) throw new ItdConfigError("auth.captcha должен быть объектом");
104
+ const { type, token, getToken, field } = captcha;
105
+ requireCaptchaText(type, "auth.captcha.type");
106
+ requireCaptchaText(token, "auth.captcha.token");
107
+ requireCaptchaText(field, "auth.captcha.field");
108
+ if (getToken !== void 0 && typeof getToken !== "function") throw new ItdConfigError("auth.captcha.getToken должен быть функцией");
109
+ }
110
+ /** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
111
+ function readCaptchaProvider(body) {
112
+ const config = isRecord(body) ? body : void 0;
113
+ const provider = config?.provider;
114
+ const field = config?.field;
115
+ if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
116
+ return {
117
+ provider,
118
+ field
119
+ };
120
+ }
121
+ /** Куда класть токен: настройка, иначе умолчание провайдера. */
122
+ function resolveField(type, configured) {
123
+ const field = configured ?? CAPTCHA_FIELDS[type];
124
+ if (field) return field;
125
+ throw new ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте auth.captcha.field либо оставьте auth.captcha.type = ${CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
126
+ }
127
+ /** Спрашивает свежий токен, затем берёт разовый. */
128
+ async function resolveToken(captcha, type) {
129
+ const token = await captcha.getToken?.(type) || captcha.token;
130
+ if (token) return token;
131
+ throw new ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте auth.captcha.getToken.`);
132
+ }
133
+ /**
134
+ * Готовит фрагмент тела запроса с токеном капчи.
135
+ *
136
+ * При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
137
+ * и имя поля приходят оттуда. Явно названный тип решается без этого запроса.
138
+ *
139
+ * @param askProvider как спросить активного провайдера; вызывается, только если это нужно
140
+ * @throws {ItdConfigError} если капча не настроена или токен не получен
141
+ */
142
+ async function resolveCaptchaBody(captcha, askProvider) {
143
+ if (!captcha?.token && !captcha?.getToken) throw new ItdConfigError("Вход по email и паролю требует токен капчи. Передайте auth.captcha.getToken — источник свежего токена — либо разовый auth.captcha.token. В Node токен получает пакет @itd-api/captcha.");
144
+ const choice = captcha.type ?? CaptchaChoice.Auto;
145
+ if (choice === CaptchaChoice.Auto) {
146
+ const { provider, field } = await askProvider();
147
+ return { [field]: await resolveToken(captcha, provider) };
148
+ }
149
+ return { [resolveField(choice, captcha.field)]: await resolveToken(captcha, choice) };
150
+ }
151
+ /**
152
+ * Готовит фрагмент тела для разового вызова с готовым доказательством.
153
+ *
154
+ * Поле берётся из самого доказательства либо из умолчания провайдера; запроса к серверу нет.
155
+ *
156
+ * @throws {ItdConfigError} если доказательство неполное
157
+ */
158
+ function captchaBody(captcha) {
159
+ if (!isRecord(captcha)) throw new ItdConfigError("captcha должен быть объектом { type, token }");
160
+ if (typeof captcha.token !== "string" || captcha.token.trim() === "") throw new ItdConfigError("captcha.token должен быть непустой строкой");
161
+ if (typeof captcha.type !== "string" || captcha.type.trim() === "") throw new ItdConfigError("captcha.type должен быть непустой строкой");
162
+ return { [resolveField(captcha.type, captcha.field)]: captcha.token };
163
+ }
61
164
  //#endregion
62
165
  //#region src/session/jwt.ts
63
166
  /**
@@ -74,38 +177,34 @@ function decodeBase64Url(segment) {
74
177
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
75
178
  return new TextDecoder().decode(bytes);
76
179
  }
77
- /**
78
- * Читает `sub` и `sid` из полезной нагрузки JWT.
79
- *
80
- * Подпись не проверяется — ключа для проверки у клиента нет. Прочитанные значения служат
81
- * лишь метками для разделения локального состояния и не дают доступа: его определяет сервер.
82
- */
83
- function readTokenIdentity(token) {
180
+ /** Читает полезную нагрузку JWT без проверки подписи. */
181
+ function readTokenPayload(token) {
84
182
  try {
85
183
  const payload = token.split(".")[1];
86
- if (!payload) return {};
184
+ if (!payload) return void 0;
87
185
  const parsed = JSON.parse(decodeBase64Url(payload));
88
- if (typeof parsed !== "object" || parsed === null) return {};
89
- const { sub, sid } = parsed;
90
- return {
91
- ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
92
- ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
93
- };
186
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
187
+ return parsed;
94
188
  } catch {
95
- return {};
189
+ return;
96
190
  }
97
191
  }
98
- /**
99
- * Читает `sub` из полезной нагрузки JWT.
100
- *
101
- * @returns идентификатор владельца токена; `undefined`, если токен не JWT, повреждён
102
- * или поля `sub` в нём нет
103
- */
104
- function readTokenSubject(token) {
105
- return readTokenIdentity(token).subject;
192
+ /** Читает используемые клиентом поля JWT за один раз. */
193
+ function readTokenMetadata(token) {
194
+ const payload = readTokenPayload(token);
195
+ if (!payload) return {};
196
+ const { sub, sid, exp } = payload;
197
+ const expiresAt = typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
198
+ return {
199
+ ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
200
+ ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {},
201
+ ...expiresAt !== void 0 && Number.isFinite(expiresAt) ? { expiresAt } : {}
202
+ };
106
203
  }
107
204
  //#endregion
108
205
  //#region src/session/auth.ts
206
+ /** Запас до истечения токена на очередь и выполнение запроса. */
207
+ const ACCESS_TOKEN_REFRESH_LEEWAY_MS = 3e4;
109
208
  /**
110
209
  * Проверяет форму `auth` и сообщает о типичных ошибках понятным текстом.
111
210
  *
@@ -134,11 +233,10 @@ function validateAuth(auth) {
134
233
  return { ...auth };
135
234
  }
136
235
  if ("email" in auth || "password" in auth) {
137
- const { email, password, turnstileToken, getTurnstileToken } = auth;
236
+ const { email, password, captcha } = auth;
138
237
  if (typeof email !== "string" || email.trim() === "") throw new ItdConfigError("auth.email должен быть непустой строкой");
139
238
  if (typeof password !== "string" || password === "") throw new ItdConfigError("auth.password должен быть непустой строкой");
140
- if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") throw new ItdConfigError("auth.getTurnstileToken должен быть функцией");
141
- if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new ItdConfigError("auth.turnstileToken должен быть непустой строкой");
239
+ validateCaptchaOptions(captcha);
142
240
  return { ...auth };
143
241
  }
144
242
  throw new ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
@@ -185,6 +283,7 @@ function createItdAuth(options, deps) {
185
283
  }
186
284
  /** Пути эндпоинтов авторизации. */
187
285
  const AUTH_PATHS = {
286
+ captchaProvider: "/api/v1/auth/captcha/provider",
188
287
  signUp: "/api/v1/auth/sign-up",
189
288
  signIn: "/api/v1/auth/sign-in",
190
289
  verifyOtp: "/api/v1/auth/verify-otp",
@@ -194,7 +293,10 @@ const AUTH_PATHS = {
194
293
  forgotPassword: "/api/v1/auth/forgot-password",
195
294
  resetPassword: "/api/v1/auth/reset-password",
196
295
  changePassword: "/api/v1/auth/change-password",
197
- sessions: "/api/v1/auth/sessions"
296
+ sessions: "/api/v1/auth/sessions",
297
+ qrStart: "/api/v1/auth/qr/start",
298
+ qrClaim: "/api/v1/auth/qr/claim",
299
+ qrStream: "/api/v1/auth/qr/stream"
198
300
  };
199
301
  /**
200
302
  * Публичный ключ Cloudflare Turnstile платформы итд.com.
@@ -204,14 +306,15 @@ const AUTH_PATHS = {
204
306
  *
205
307
  * Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
206
308
  * Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
207
- * подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
309
+ * подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
208
310
  * без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
209
311
  *
210
312
  * @example
211
313
  * ```ts
212
314
  * turnstile.render('#captcha', {
213
315
  * sitekey: TURNSTILE_SITE_KEY,
214
- * callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
316
+ * callback: (token) =>
317
+ * itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
215
318
  * });
216
319
  * ```
217
320
  */
@@ -305,7 +408,7 @@ var AuthManager = class {
305
408
  * старте читают хранилище один раз и не заводят каждый свой `deviceId`.
306
409
  */
307
410
  #loading = null;
308
- /** Общий промис обновления: к нему присоединяются все, кто получил 401. */
411
+ /** Текущее обновление: к нему присоединяются запросы той же ревизии. */
309
412
  #refreshing = null;
310
413
  /** Общий промис входа по логину и паролю. */
311
414
  #signingIn = null;
@@ -315,6 +418,7 @@ var AuthManager = class {
315
418
  #externalToken;
316
419
  /** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
317
420
  #externalIdentity;
421
+ #tokenMetadata;
318
422
  /**
319
423
  * Идентификатор устройства.
320
424
  *
@@ -328,9 +432,7 @@ var AuthManager = class {
328
432
  * Счётчик смен владельца авторизации.
329
433
  *
330
434
  * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
331
- * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
332
- * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
333
- * воскресить уже очищенную сессию поверх `signOut`.
435
+ * `setSession`, `setAccessToken`, вход. Refresh сверяет снимок до и после сетевого запроса.
334
436
  */
335
437
  #authEpoch = 0;
336
438
  #externalReadGeneration = 0;
@@ -363,6 +465,7 @@ var AuthManager = class {
363
465
  this.#disposed = true;
364
466
  this.#externalReadGeneration += 1;
365
467
  this.#invalidateInFlight();
468
+ this.#tokenMetadata = void 0;
366
469
  this.#emitter.removeAllListeners();
367
470
  }
368
471
  /**
@@ -423,12 +526,25 @@ var AuthManager = class {
423
526
  return this.#session?.accessToken ?? null;
424
527
  }
425
528
  #identityForToken(accessToken) {
426
- const token = accessToken ? readTokenIdentity(accessToken) : {};
529
+ const token = this.#metadataForToken(accessToken);
427
530
  return {
428
531
  ...token.subject ? { userId: token.subject } : {},
429
532
  ...token.sessionId ? { sessionId: token.sessionId } : {}
430
533
  };
431
534
  }
535
+ #metadataForToken(accessToken) {
536
+ if (!accessToken) {
537
+ this.#tokenMetadata = void 0;
538
+ return {};
539
+ }
540
+ if (this.#tokenMetadata?.accessToken === accessToken) return this.#tokenMetadata.value;
541
+ const value = readTokenMetadata(accessToken);
542
+ this.#tokenMetadata = {
543
+ accessToken,
544
+ value
545
+ };
546
+ return value;
547
+ }
432
548
  async #readExternalToken(getToken) {
433
549
  this.#assertActive();
434
550
  const generation = ++this.#externalReadGeneration;
@@ -475,15 +591,33 @@ var AuthManager = class {
475
591
  if (this.#jar.has("is_auth", this.#config.baseUrl)) return true;
476
592
  return Boolean(this.#session?.refreshToken);
477
593
  }
478
- /**
479
- * Готовит состояние авторизации до входа запроса в очередь. Стадия `auth_preparation`.
480
- *
481
- * Загрузка хранилища, внешний источник токена и отложенный вход асинхронны, поэтому
482
- * обязаны завершиться до захвата слота очереди.
483
- */
594
+ /** Готовит внутреннюю сессию до retry исходного запроса. */
595
+ async preflight(allowRefresh) {
596
+ this.#assertActive();
597
+ let accessToken = (await this.#loadSession())?.accessToken;
598
+ if (!accessToken) {
599
+ const auth = this.#config.auth;
600
+ if (typeof auth === "object" && auth !== null && "email" in auth) accessToken = await this.#signInWithCredentials(auth) ?? void 0;
601
+ }
602
+ if (!accessToken || !allowRefresh || !this.#config.autoRefresh) return;
603
+ const expiresAt = this.#metadataForToken(accessToken).expiresAt;
604
+ if (expiresAt === void 0 || expiresAt > this.#config.clock.now() + ACCESS_TOKEN_REFRESH_LEEWAY_MS || !this.#hasRefreshSession()) return;
605
+ try {
606
+ await this.#refreshDeduplicated({
607
+ epoch: this.#authEpoch,
608
+ accessToken
609
+ });
610
+ } catch (error) {
611
+ this.#emitter.emit("authError", { error });
612
+ throw error;
613
+ }
614
+ }
615
+ /** Перед каждой попыткой перечитывает внешний источник токена. */
484
616
  async prepare() {
485
617
  this.#assertActive();
486
- await this.token();
618
+ if ((await this.#loadSession())?.accessToken) return;
619
+ const auth = this.#config.auth;
620
+ if (typeof auth === "object" && auth !== null && "getToken" in auth) await this.#readExternalToken(() => auth.getToken());
487
621
  }
488
622
  /**
489
623
  * Заголовки уже подготовленной авторизации без чтения storage или вызова внешнего источника.
@@ -598,6 +732,13 @@ var AuthManager = class {
598
732
  }
599
733
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
600
734
  async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
735
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
736
+ }
737
+ /** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
738
+ async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
739
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
740
+ }
741
+ async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
601
742
  this.#assertActive();
602
743
  if (typeof accessToken !== "string" || accessToken.trim() === "") throw new ItdConfigError("accessToken должен быть непустой строкой");
603
744
  await this.#loadSession();
@@ -607,7 +748,7 @@ var AuthManager = class {
607
748
  this.#invalidateInFlight();
608
749
  this.#transitionAuth(accessToken);
609
750
  const saved = this.#saveSession({
610
- ...this.#session ?? {},
751
+ ...replaceSession ? {} : this.#session ?? {},
611
752
  accessToken,
612
753
  obtainedAt: this.#config.clock.now()
613
754
  });
@@ -630,7 +771,7 @@ var AuthManager = class {
630
771
  async getUserId() {
631
772
  this.#assertActive();
632
773
  const session = await this.#loadSession();
633
- return session?.accessToken ? readTokenSubject(session.accessToken) : void 0;
774
+ return session?.accessToken ? this.#identityForToken(session.accessToken).userId : void 0;
634
775
  }
635
776
  /** Заменяет сессию и связанные с ней cookie целиком. */
636
777
  async setSession(session) {
@@ -733,24 +874,30 @@ var AuthManager = class {
733
874
  this.#persistence = pending.catch(() => {});
734
875
  return pending;
735
876
  }
736
- /**
737
- * Обновление с дедупликацией.
738
- *
739
- * Все, кто пришёл, пока обновление уже идёт, получают его результат, а не запускают своё.
740
- */
741
- #refreshDeduplicated() {
742
- if (this.#refreshing) return this.#refreshing;
743
- const promise = this.#performRefresh().finally(() => {
744
- this.#refreshing = null;
877
+ /** Дедуплицирует refresh одной ревизии; следующая ждёт завершения предыдущей. */
878
+ #refreshDeduplicated(expectation = { epoch: this.#authEpoch }) {
879
+ const current = this.#refreshing;
880
+ if (current) {
881
+ if (current.epoch === expectation.epoch) return current.promise;
882
+ return current.promise.then(() => this.#refreshDeduplicated(expectation), () => this.#refreshDeduplicated(expectation));
883
+ }
884
+ const promise = this.#performRefresh(expectation).finally(() => {
885
+ if (this.#refreshing?.promise === promise) this.#refreshing = null;
745
886
  });
746
- this.#refreshing = promise;
887
+ this.#refreshing = {
888
+ epoch: expectation.epoch,
889
+ promise
890
+ };
747
891
  return promise;
748
892
  }
749
- async #performRefresh() {
893
+ #matchesRefreshExpectation(expectation) {
894
+ return this.#authEpoch === expectation.epoch && (expectation.accessToken === void 0 || this.#session?.accessToken === expectation.accessToken);
895
+ }
896
+ async #performRefresh(expectation) {
750
897
  this.#assertActive();
751
898
  await this.#loadSession();
899
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#currentAccessToken();
752
900
  if (!this.#hasRefreshSession()) return this.#reloginOrNull();
753
- const epoch = this.#authEpoch;
754
901
  const flowCookies = this.#jar.clone();
755
902
  try {
756
903
  const accessToken = await this.#http.execute(AUTH_REFRESH, {
@@ -759,7 +906,7 @@ var AuthManager = class {
759
906
  skipAuthRefresh: true,
760
907
  cookieJar: flowCookies
761
908
  });
762
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
909
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#session?.accessToken ?? null;
763
910
  this.#jar.replaceWith(flowCookies);
764
911
  const rotated = this.#jar.getValue(REFRESH_COOKIE, this.#config.baseUrl + REFRESH_COOKIE_PATH);
765
912
  this.#transitionAuth(accessToken, false);
@@ -771,10 +918,11 @@ var AuthManager = class {
771
918
  });
772
919
  this.#emitter.emit("tokens", { accessToken });
773
920
  await saved;
921
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
774
922
  return accessToken;
775
923
  } catch (error) {
924
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
776
925
  if (error instanceof ItdAuthError) {
777
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
778
926
  this.#invalidateInFlight();
779
927
  const clearedRevision = this.#authEpoch;
780
928
  this.#transitionAuth(void 0);
@@ -815,29 +963,23 @@ var AuthManager = class {
815
963
  this.#signingIn = promise;
816
964
  return promise;
817
965
  }
818
- /**
819
- * Берёт токен капчи для входа.
820
- *
821
- * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
822
- * несколько минут, поэтому при повторном входе через сутки годится только свежий.
823
- */
824
- async #resolveTurnstileToken(credentials) {
825
- if (credentials.getTurnstileToken) {
826
- const token = await credentials.getTurnstileToken();
827
- if (token) return token;
828
- }
829
- if (credentials.turnstileToken) return credentials.turnstileToken;
830
- throw new ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета — TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
966
+ /** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
967
+ async #resolveCaptchaProvider() {
968
+ return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
969
+ path: AUTH_PATHS.captchaProvider,
970
+ skipAuth: true,
971
+ skipAuthRefresh: true
972
+ }));
831
973
  }
832
974
  async #performSignIn(credentials, revision) {
833
- const turnstileToken = await this.#resolveTurnstileToken(credentials);
975
+ const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
834
976
  const flowCookies = new CookieJar();
835
977
  const result = await this.#http.execute(AUTH_SIGN_IN, {
836
978
  path: AUTH_PATHS.signIn,
837
979
  body: {
838
980
  email: credentials.email,
839
981
  password: credentials.password,
840
- turnstileToken
982
+ ...captcha
841
983
  },
842
984
  skipAuth: true,
843
985
  skipAuthRefresh: true,
@@ -861,6 +1003,125 @@ var AuthManager = class {
861
1003
  }
862
1004
  };
863
1005
  //#endregion
1006
+ //#region src/session/qr-stream.ts
1007
+ const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
1008
+ function readQrLoginStreamEvent(data) {
1009
+ let value;
1010
+ try {
1011
+ value = JSON.parse(data);
1012
+ } catch {
1013
+ return;
1014
+ }
1015
+ if (!isRecord(value)) return void 0;
1016
+ const event = value;
1017
+ if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
1018
+ return {
1019
+ status: event.status,
1020
+ ...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
1021
+ };
1022
+ }
1023
+ /** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
1024
+ async function consumeQrLoginStream(body, onEvent, signal) {
1025
+ const reader = body.getReader();
1026
+ const decoder = new TextDecoder();
1027
+ let delivery = Promise.resolve();
1028
+ let completed = false;
1029
+ const cancel = () => {
1030
+ reader.cancel(signal?.reason).catch(() => {});
1031
+ };
1032
+ signal?.addEventListener("abort", cancel, { once: true });
1033
+ const parser = createParser({ onEvent(message) {
1034
+ const event = readQrLoginStreamEvent(message.data);
1035
+ if (!event) return;
1036
+ delivery = delivery.then(() => onEvent(event));
1037
+ } });
1038
+ try {
1039
+ for (;;) {
1040
+ const { done, value } = await reader.read();
1041
+ if (done) break;
1042
+ parser.feed(decoder.decode(value, { stream: true }));
1043
+ await delivery;
1044
+ }
1045
+ parser.feed(decoder.decode());
1046
+ await delivery;
1047
+ if (signal?.aborted) throw signal.reason;
1048
+ completed = true;
1049
+ } finally {
1050
+ signal?.removeEventListener("abort", cancel);
1051
+ if (!completed) await reader.cancel().catch(() => {});
1052
+ reader.releaseLock?.();
1053
+ }
1054
+ }
1055
+ /** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
1056
+ async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
1057
+ connection.assertActive?.("открыть QR-поток");
1058
+ const path = AUTH_PATHS.qrStream;
1059
+ const method = "POST";
1060
+ const url = joinUrl(connection.baseUrl, path);
1061
+ const abort = createRequestAbortScope(signal, connection.signal, 0, connection.clock);
1062
+ try {
1063
+ const headers = await connection.baseHeaders(url);
1064
+ headers.delete("Authorization");
1065
+ headers.delete("Cookie");
1066
+ headers.set("Accept", "text/event-stream");
1067
+ headers.set("Content-Type", "application/json");
1068
+ const cookie = cookies.getHeader(url);
1069
+ if (cookie) headers.set("Cookie", cookie);
1070
+ let response;
1071
+ try {
1072
+ response = await connection.fetch(url, {
1073
+ method,
1074
+ headers,
1075
+ body: JSON.stringify(input),
1076
+ credentials: "include",
1077
+ signal: abort.signal
1078
+ });
1079
+ } catch (error) {
1080
+ const failure = new ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
1081
+ method,
1082
+ path,
1083
+ cause: error
1084
+ });
1085
+ throw requestAbortError(abort, {
1086
+ timeout: 0,
1087
+ method,
1088
+ path
1089
+ }, failure);
1090
+ }
1091
+ cookies.setFromResponse(response.url || url, response);
1092
+ if (!response.ok) {
1093
+ const text = await response.text();
1094
+ let body = text;
1095
+ try {
1096
+ body = JSON.parse(text);
1097
+ } catch {}
1098
+ throw createApiError({
1099
+ method,
1100
+ path,
1101
+ status: response.status,
1102
+ statusText: response.statusText,
1103
+ headers: response.headers,
1104
+ response,
1105
+ body,
1106
+ now: connection.clock.now()
1107
+ });
1108
+ }
1109
+ if (!response.body) throw new ItdNetworkError("Сервер не вернул тело QR-потока", {
1110
+ method,
1111
+ path
1112
+ });
1113
+ await consumeQrLoginStream(response.body, onEvent, abort.signal);
1114
+ } catch (error) {
1115
+ throw requestAbortError(abort, {
1116
+ timeout: 0,
1117
+ method,
1118
+ path
1119
+ }, error);
1120
+ } finally {
1121
+ abort.cleanup();
1122
+ }
1123
+ }
1124
+ //#endregion
864
1125
  //#region src/resources/auth.ts
865
1126
  /**
866
1127
  * Результат входа.
@@ -875,9 +1136,35 @@ var AuthManager = class {
875
1136
  */
876
1137
  var AuthResource = class extends BaseResource {
877
1138
  #auth;
1139
+ #connection;
1140
+ #qrFlows = /* @__PURE__ */ new Map();
878
1141
  constructor(http, deps) {
879
1142
  super(http);
880
1143
  this.#auth = deps.auth;
1144
+ this.#connection = deps.connection;
1145
+ }
1146
+ #pruneQrFlows() {
1147
+ const now = this.#connection.clock.now();
1148
+ for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
1149
+ }
1150
+ #qrFlow(qrId) {
1151
+ this.#pruneQrFlows();
1152
+ const existing = this.#qrFlows.get(qrId);
1153
+ if (existing) return existing;
1154
+ const flow = {
1155
+ cookies: this.#auth.createCookieFlow(false),
1156
+ expiresAt: void 0
1157
+ };
1158
+ this.#qrFlows.set(qrId, flow);
1159
+ return flow;
1160
+ }
1161
+ #touchQrFlow(flow, expiresIn) {
1162
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
1163
+ const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
1164
+ flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
1165
+ }
1166
+ #forgetTerminalQrError(qrId, error) {
1167
+ if (isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
881
1168
  }
882
1169
  /**
883
1170
  * Проверяет состояние авторизации и возвращает текущего пользователя.
@@ -891,14 +1178,106 @@ var AuthResource = class extends BaseResource {
891
1178
  });
892
1179
  }
893
1180
  /**
1181
+ * Узнаёт активного провайдера капчи и поле токена.
1182
+ *
1183
+ * Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
1184
+ * Автоматическому входу этот вызов не нужен — клиент делает его сам.
1185
+ */
1186
+ captchaProvider(options = {}) {
1187
+ return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
1188
+ path: AUTH_PATHS.captchaProvider,
1189
+ skipAuth: true,
1190
+ skipAuthRefresh: true,
1191
+ ...options
1192
+ });
1193
+ }
1194
+ /** Создаёт короткоживущую сессию QR-входа. */
1195
+ async startQrLogin(options = {}) {
1196
+ const cookies = this.#auth.createCookieFlow(false);
1197
+ const result = await this.http.execute(AUTH_QR_START, {
1198
+ path: AUTH_PATHS.qrStart,
1199
+ skipAuth: true,
1200
+ skipAuthRefresh: true,
1201
+ cookieJar: cookies,
1202
+ ...options
1203
+ });
1204
+ if (typeof result.qrId === "string" && result.qrId !== "") {
1205
+ const flow = {
1206
+ cookies,
1207
+ expiresAt: void 0
1208
+ };
1209
+ this.#touchQrFlow(flow, result.expiresIn);
1210
+ this.#qrFlows.set(result.qrId, flow);
1211
+ }
1212
+ return result;
1213
+ }
1214
+ /**
1215
+ * Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
1216
+ *
1217
+ * До подтверждения возвращает промежуточный статус. Если сервер потребовал капчу,
1218
+ * повторите вызов с `captcha` — тип берётся из {@link captchaProvider}.
1219
+ * Полученный `accessToken` автоматически сохраняется в клиенте.
1220
+ */
1221
+ async claimQrLogin(input, options = {}) {
1222
+ const revision = this.#auth.revision();
1223
+ const flow = this.#qrFlow(input.qrId);
1224
+ try {
1225
+ const { captcha, ...rest } = input;
1226
+ const result = await this.http.execute(AUTH_QR_CLAIM, {
1227
+ path: AUTH_PATHS.qrClaim,
1228
+ body: {
1229
+ ...rest,
1230
+ ...captcha ? captchaBody(captcha) : {}
1231
+ },
1232
+ skipAuth: true,
1233
+ skipAuthRefresh: true,
1234
+ cookieJar: flow.cookies,
1235
+ ...options
1236
+ });
1237
+ this.#touchQrFlow(flow, result.expiresIn);
1238
+ if (result.status === QrLoginStatus.Authorized) {
1239
+ await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
1240
+ this.#qrFlows.delete(input.qrId);
1241
+ } else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
1242
+ return result;
1243
+ } catch (error) {
1244
+ this.#forgetTerminalQrError(input.qrId, error);
1245
+ throw error;
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
1250
+ *
1251
+ * Событие `approved` означает, что подтверждение завершено: после него вызовите
1252
+ * {@link claimQrLogin}, чтобы получить и сохранить access token.
1253
+ */
1254
+ async streamQrLogin(input, onEvent, options = {}) {
1255
+ if (typeof onEvent !== "function") throw new ItdConfigError("onEvent QR-потока должен быть функцией");
1256
+ const flow = this.#qrFlow(input.qrId);
1257
+ try {
1258
+ await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
1259
+ this.#touchQrFlow(flow, event.expiresIn);
1260
+ if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
1261
+ await onEvent(event);
1262
+ }, options.signal);
1263
+ } catch (error) {
1264
+ this.#forgetTerminalQrError(input.qrId, error);
1265
+ throw error;
1266
+ }
1267
+ }
1268
+ /**
894
1269
  * Регистрирует аккаунт и запускает подтверждение по коду.
895
1270
  *
896
1271
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
897
1272
  */
898
1273
  signUp(credentials, options = {}) {
1274
+ const { captcha, ...rest } = credentials;
899
1275
  return this.http.execute(AUTH_SIGN_UP, {
900
1276
  path: AUTH_PATHS.signUp,
901
- body: credentials,
1277
+ body: {
1278
+ ...rest,
1279
+ ...captchaBody(captcha)
1280
+ },
902
1281
  skipAuth: true,
903
1282
  skipAuthRefresh: true,
904
1283
  ...options
@@ -915,17 +1294,21 @@ var AuthResource = class extends BaseResource {
915
1294
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
916
1295
  */
917
1296
  async signIn(credentials, options = {}) {
1297
+ const { captcha, ...rest } = credentials;
918
1298
  const revision = this.#auth.revision();
919
1299
  const cookies = this.#auth.createCookieFlow(false);
920
1300
  const result = await this.http.execute(AUTH_SIGN_IN, {
921
1301
  path: AUTH_PATHS.signIn,
922
- body: credentials,
1302
+ body: {
1303
+ ...rest,
1304
+ ...captchaBody(captcha)
1305
+ },
923
1306
  skipAuth: true,
924
1307
  skipAuthRefresh: true,
925
1308
  cookieJar: cookies,
926
1309
  ...options
927
1310
  });
928
- if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken, revision, cookies);
1311
+ if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
929
1312
  else this.#auth.commitCookieFlow(cookies, revision);
930
1313
  return result;
931
1314
  }
@@ -945,7 +1328,7 @@ var AuthResource = class extends BaseResource {
945
1328
  cookieJar: cookies,
946
1329
  ...options
947
1330
  });
948
- await this.#auth.setAccessToken(accessToken, revision, cookies);
1331
+ await this.#auth.commitAuthFlow(accessToken, cookies, revision);
949
1332
  return accessToken;
950
1333
  }
951
1334
  /** Отправляет код подтверждения повторно. */
@@ -1050,7 +1433,10 @@ var AuthResource = class extends BaseResource {
1050
1433
  forgotPassword(input, options = {}) {
1051
1434
  return this.http.execute(AUTH_FORGOT_PASSWORD, {
1052
1435
  path: AUTH_PATHS.forgotPassword,
1053
- body: input,
1436
+ body: {
1437
+ email: input.email,
1438
+ ...captchaBody(input.captcha)
1439
+ },
1054
1440
  skipAuth: true,
1055
1441
  skipAuthRefresh: true,
1056
1442
  ...options
@@ -1081,23 +1467,21 @@ var AuthResource = class extends BaseResource {
1081
1467
  * ```ts
1082
1468
  * await itd.auth.resetPasswordWithOtp({
1083
1469
  * email,
1084
- * turnstileToken,
1470
+ * captcha: { type: provider, token, field },
1085
1471
  * newPassword,
1086
1472
  * getOtp: () => rl.question('Код из письма: '),
1087
1473
  * });
1088
1474
  * ```
1089
1475
  */
1090
1476
  async resetPasswordWithOtp(input, options = {}) {
1091
- const flowToken = await this.forgotPassword({
1092
- email: input.email,
1093
- turnstileToken: input.turnstileToken
1094
- }, options);
1095
- const otp = await input.getOtp();
1477
+ const { getOtp, newPassword, ...request } = input;
1478
+ const flowToken = await this.forgotPassword(request, options);
1479
+ const otp = await getOtp();
1096
1480
  await this.resetPassword({
1097
1481
  email: input.email,
1098
1482
  otp,
1099
1483
  flowToken,
1100
- newPassword: input.newPassword
1484
+ newPassword
1101
1485
  }, options);
1102
1486
  }
1103
1487
  /**
@@ -1186,8 +1570,8 @@ function createManagedClient(options, internals) {
1186
1570
  * const itd = new ItdClient({
1187
1571
  * // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
1188
1572
  * // а истёкший продлевается сам. Здесь он нужен на первый запуск.
1189
- * // Вход по паролю требует токена капчи — см. AuthInput и TURNSTILE_SITE_KEY.
1190
- * auth: { email, password, getTurnstileToken },
1573
+ * // Вход по паролю требует токена капчи — см. CaptchaOptions.
1574
+ * auth: { email, password, captcha: createCaptchaSolver() },
1191
1575
  * storage: new FileTokenStorage('./.itd-session.json'),
1192
1576
  * rateLimit: { concurrency: 4, rps: 8 },
1193
1577
  * });
@@ -1208,7 +1592,10 @@ var ItdClient = class ItdClient {
1208
1592
  #auth;
1209
1593
  /** Авторизация, сессии и пароли. */
1210
1594
  get auth() {
1211
- this.#auth ??= new AuthResource(this.#runtime.http, { auth: this.#runtime.auth });
1595
+ this.#auth ??= new AuthResource(this.#runtime.http, {
1596
+ auth: this.#runtime.auth,
1597
+ connection: this.#runtime.connection()
1598
+ });
1212
1599
  return this.#auth;
1213
1600
  }
1214
1601
  /** Профили, подписки, блокировки, приватность. */
@@ -1705,7 +2092,9 @@ function assertAccountsActive(accounts, action) {
1705
2092
  * await accounts.restore();
1706
2093
  *
1707
2094
  * if (!accounts.has('kiow')) {
1708
- * accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
2095
+ * accounts.addAccount('kiow', {
2096
+ * auth: { email, password, captcha: createCaptchaSolver() },
2097
+ * });
1709
2098
  * }
1710
2099
  *
1711
2100
  * await accounts.account('kiow').posts.create({ content: 'привет' });
@@ -1784,7 +2173,9 @@ var ItdAccounts = class ItdAccounts {
1784
2173
  *
1785
2174
  * @example
1786
2175
  * ```ts
1787
- * accounts.addAccount('bot', { auth: { email, password, getTurnstileToken } });
2176
+ * accounts.addAccount('bot', {
2177
+ * auth: { email, password, captcha: createCaptchaSolver() },
2178
+ * });
1788
2179
  * accounts.addAccount('reader', { auth: '<accessToken>' });
1789
2180
  * accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
1790
2181
  * ```
@@ -2117,6 +2508,6 @@ function createAccounts(options = {}) {
2117
2508
  return new ItdAccounts(options);
2118
2509
  }
2119
2510
  //#endregion
2120
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, EventChannel, EventChannelStatus, EventComposer, EventRouter, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryShopOrderAccessStorage, MemoryTokenStorage, NotificationEvents, NotificationEventsClient, NotificationEventsTransport, NotificationType, NotificationUpdateOrigin, NotificationUpdateType, OPERATIONS, PaginationMode, Paginator, RateLimitPacing, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, ShopConsentKind, ShopOrderStatus, ShopProductCategory, ShopProductStatus, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createNotificationEventsClient, createRecordKeyValueStore, createShopFeature, createShopOrderAccessStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runEventMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
2511
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CAPTCHA_FIELDS, CaptchaChoice, CaptchaField, CaptchaType, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, EventChannel, EventChannelStatus, EventComposer, EventRouter, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryShopOrderAccessStorage, MemoryTokenStorage, NotificationEvents, NotificationEventsClient, NotificationEventsTransport, NotificationType, NotificationUpdateOrigin, NotificationUpdateType, OPERATIONS, PaginationMode, Paginator, QrLoginStatus, QrLoginStreamStatus, RateLimitPacing, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, ShopConsentKind, ShopOrderStatus, ShopProductCategory, ShopProductStatus, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createNotificationEventsClient, createRecordKeyValueStore, createShopFeature, createShopOrderAccessStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runEventMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
2121
2512
 
2122
2513
  //# sourceMappingURL=index.js.map