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.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-3Ts6EEHQ.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-CtOpp9Pm.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-BzjI6pn5.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
  /**
@@ -130,11 +233,10 @@ function validateAuth(auth) {
130
233
  return { ...auth };
131
234
  }
132
235
  if ("email" in auth || "password" in auth) {
133
- const { email, password, turnstileToken, getTurnstileToken } = auth;
236
+ const { email, password, captcha } = auth;
134
237
  if (typeof email !== "string" || email.trim() === "") throw new ItdConfigError("auth.email должен быть непустой строкой");
135
238
  if (typeof password !== "string" || password === "") throw new ItdConfigError("auth.password должен быть непустой строкой");
136
- if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") throw new ItdConfigError("auth.getTurnstileToken должен быть функцией");
137
- if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new ItdConfigError("auth.turnstileToken должен быть непустой строкой");
239
+ validateCaptchaOptions(captcha);
138
240
  return { ...auth };
139
241
  }
140
242
  throw new ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
@@ -181,6 +283,7 @@ function createItdAuth(options, deps) {
181
283
  }
182
284
  /** Пути эндпоинтов авторизации. */
183
285
  const AUTH_PATHS = {
286
+ captchaProvider: "/api/v1/auth/captcha/provider",
184
287
  signUp: "/api/v1/auth/sign-up",
185
288
  signIn: "/api/v1/auth/sign-in",
186
289
  verifyOtp: "/api/v1/auth/verify-otp",
@@ -190,7 +293,10 @@ const AUTH_PATHS = {
190
293
  forgotPassword: "/api/v1/auth/forgot-password",
191
294
  resetPassword: "/api/v1/auth/reset-password",
192
295
  changePassword: "/api/v1/auth/change-password",
193
- 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"
194
300
  };
195
301
  /**
196
302
  * Публичный ключ Cloudflare Turnstile платформы итд.com.
@@ -200,14 +306,15 @@ const AUTH_PATHS = {
200
306
  *
201
307
  * Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
202
308
  * Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
203
- * подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
309
+ * подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
204
310
  * без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
205
311
  *
206
312
  * @example
207
313
  * ```ts
208
314
  * turnstile.render('#captcha', {
209
315
  * sitekey: TURNSTILE_SITE_KEY,
210
- * callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
316
+ * callback: (token) =>
317
+ * itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
211
318
  * });
212
319
  * ```
213
320
  */
@@ -625,6 +732,13 @@ var AuthManager = class {
625
732
  }
626
733
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
627
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) {
628
742
  this.#assertActive();
629
743
  if (typeof accessToken !== "string" || accessToken.trim() === "") throw new ItdConfigError("accessToken должен быть непустой строкой");
630
744
  await this.#loadSession();
@@ -634,7 +748,7 @@ var AuthManager = class {
634
748
  this.#invalidateInFlight();
635
749
  this.#transitionAuth(accessToken);
636
750
  const saved = this.#saveSession({
637
- ...this.#session ?? {},
751
+ ...replaceSession ? {} : this.#session ?? {},
638
752
  accessToken,
639
753
  obtainedAt: this.#config.clock.now()
640
754
  });
@@ -849,29 +963,23 @@ var AuthManager = class {
849
963
  this.#signingIn = promise;
850
964
  return promise;
851
965
  }
852
- /**
853
- * Берёт токен капчи для входа.
854
- *
855
- * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
856
- * несколько минут, поэтому при повторном входе через сутки годится только свежий.
857
- */
858
- async #resolveTurnstileToken(credentials) {
859
- if (credentials.getTurnstileToken) {
860
- const token = await credentials.getTurnstileToken();
861
- if (token) return token;
862
- }
863
- if (credentials.turnstileToken) return credentials.turnstileToken;
864
- 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
+ }));
865
973
  }
866
974
  async #performSignIn(credentials, revision) {
867
- const turnstileToken = await this.#resolveTurnstileToken(credentials);
975
+ const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
868
976
  const flowCookies = new CookieJar();
869
977
  const result = await this.#http.execute(AUTH_SIGN_IN, {
870
978
  path: AUTH_PATHS.signIn,
871
979
  body: {
872
980
  email: credentials.email,
873
981
  password: credentials.password,
874
- turnstileToken
982
+ ...captcha
875
983
  },
876
984
  skipAuth: true,
877
985
  skipAuthRefresh: true,
@@ -895,6 +1003,125 @@ var AuthManager = class {
895
1003
  }
896
1004
  };
897
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
898
1125
  //#region src/resources/auth.ts
899
1126
  /**
900
1127
  * Результат входа.
@@ -909,9 +1136,35 @@ var AuthManager = class {
909
1136
  */
910
1137
  var AuthResource = class extends BaseResource {
911
1138
  #auth;
1139
+ #connection;
1140
+ #qrFlows = /* @__PURE__ */ new Map();
912
1141
  constructor(http, deps) {
913
1142
  super(http);
914
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);
915
1168
  }
916
1169
  /**
917
1170
  * Проверяет состояние авторизации и возвращает текущего пользователя.
@@ -925,14 +1178,106 @@ var AuthResource = class extends BaseResource {
925
1178
  });
926
1179
  }
927
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
+ /**
928
1269
  * Регистрирует аккаунт и запускает подтверждение по коду.
929
1270
  *
930
1271
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
931
1272
  */
932
1273
  signUp(credentials, options = {}) {
1274
+ const { captcha, ...rest } = credentials;
933
1275
  return this.http.execute(AUTH_SIGN_UP, {
934
1276
  path: AUTH_PATHS.signUp,
935
- body: credentials,
1277
+ body: {
1278
+ ...rest,
1279
+ ...captchaBody(captcha)
1280
+ },
936
1281
  skipAuth: true,
937
1282
  skipAuthRefresh: true,
938
1283
  ...options
@@ -949,17 +1294,21 @@ var AuthResource = class extends BaseResource {
949
1294
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
950
1295
  */
951
1296
  async signIn(credentials, options = {}) {
1297
+ const { captcha, ...rest } = credentials;
952
1298
  const revision = this.#auth.revision();
953
1299
  const cookies = this.#auth.createCookieFlow(false);
954
1300
  const result = await this.http.execute(AUTH_SIGN_IN, {
955
1301
  path: AUTH_PATHS.signIn,
956
- body: credentials,
1302
+ body: {
1303
+ ...rest,
1304
+ ...captchaBody(captcha)
1305
+ },
957
1306
  skipAuth: true,
958
1307
  skipAuthRefresh: true,
959
1308
  cookieJar: cookies,
960
1309
  ...options
961
1310
  });
962
- 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);
963
1312
  else this.#auth.commitCookieFlow(cookies, revision);
964
1313
  return result;
965
1314
  }
@@ -979,7 +1328,7 @@ var AuthResource = class extends BaseResource {
979
1328
  cookieJar: cookies,
980
1329
  ...options
981
1330
  });
982
- await this.#auth.setAccessToken(accessToken, revision, cookies);
1331
+ await this.#auth.commitAuthFlow(accessToken, cookies, revision);
983
1332
  return accessToken;
984
1333
  }
985
1334
  /** Отправляет код подтверждения повторно. */
@@ -1084,7 +1433,10 @@ var AuthResource = class extends BaseResource {
1084
1433
  forgotPassword(input, options = {}) {
1085
1434
  return this.http.execute(AUTH_FORGOT_PASSWORD, {
1086
1435
  path: AUTH_PATHS.forgotPassword,
1087
- body: input,
1436
+ body: {
1437
+ email: input.email,
1438
+ ...captchaBody(input.captcha)
1439
+ },
1088
1440
  skipAuth: true,
1089
1441
  skipAuthRefresh: true,
1090
1442
  ...options
@@ -1115,23 +1467,21 @@ var AuthResource = class extends BaseResource {
1115
1467
  * ```ts
1116
1468
  * await itd.auth.resetPasswordWithOtp({
1117
1469
  * email,
1118
- * turnstileToken,
1470
+ * captcha: { type: provider, token, field },
1119
1471
  * newPassword,
1120
1472
  * getOtp: () => rl.question('Код из письма: '),
1121
1473
  * });
1122
1474
  * ```
1123
1475
  */
1124
1476
  async resetPasswordWithOtp(input, options = {}) {
1125
- const flowToken = await this.forgotPassword({
1126
- email: input.email,
1127
- turnstileToken: input.turnstileToken
1128
- }, options);
1129
- const otp = await input.getOtp();
1477
+ const { getOtp, newPassword, ...request } = input;
1478
+ const flowToken = await this.forgotPassword(request, options);
1479
+ const otp = await getOtp();
1130
1480
  await this.resetPassword({
1131
1481
  email: input.email,
1132
1482
  otp,
1133
1483
  flowToken,
1134
- newPassword: input.newPassword
1484
+ newPassword
1135
1485
  }, options);
1136
1486
  }
1137
1487
  /**
@@ -1220,8 +1570,8 @@ function createManagedClient(options, internals) {
1220
1570
  * const itd = new ItdClient({
1221
1571
  * // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
1222
1572
  * // а истёкший продлевается сам. Здесь он нужен на первый запуск.
1223
- * // Вход по паролю требует токена капчи — см. AuthInput и TURNSTILE_SITE_KEY.
1224
- * auth: { email, password, getTurnstileToken },
1573
+ * // Вход по паролю требует токена капчи — см. CaptchaOptions.
1574
+ * auth: { email, password, captcha: createCaptchaSolver() },
1225
1575
  * storage: new FileTokenStorage('./.itd-session.json'),
1226
1576
  * rateLimit: { concurrency: 4, rps: 8 },
1227
1577
  * });
@@ -1242,7 +1592,10 @@ var ItdClient = class ItdClient {
1242
1592
  #auth;
1243
1593
  /** Авторизация, сессии и пароли. */
1244
1594
  get auth() {
1245
- 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
+ });
1246
1599
  return this.#auth;
1247
1600
  }
1248
1601
  /** Профили, подписки, блокировки, приватность. */
@@ -1739,7 +2092,9 @@ function assertAccountsActive(accounts, action) {
1739
2092
  * await accounts.restore();
1740
2093
  *
1741
2094
  * if (!accounts.has('kiow')) {
1742
- * accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
2095
+ * accounts.addAccount('kiow', {
2096
+ * auth: { email, password, captcha: createCaptchaSolver() },
2097
+ * });
1743
2098
  * }
1744
2099
  *
1745
2100
  * await accounts.account('kiow').posts.create({ content: 'привет' });
@@ -1818,7 +2173,9 @@ var ItdAccounts = class ItdAccounts {
1818
2173
  *
1819
2174
  * @example
1820
2175
  * ```ts
1821
- * accounts.addAccount('bot', { auth: { email, password, getTurnstileToken } });
2176
+ * accounts.addAccount('bot', {
2177
+ * auth: { email, password, captcha: createCaptchaSolver() },
2178
+ * });
1822
2179
  * accounts.addAccount('reader', { auth: '<accessToken>' });
1823
2180
  * accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
1824
2181
  * ```
@@ -2151,6 +2508,6 @@ function createAccounts(options = {}) {
2151
2508
  return new ItdAccounts(options);
2152
2509
  }
2153
2510
  //#endregion
2154
- 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 };
2155
2512
 
2156
2513
  //# sourceMappingURL=index.js.map