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.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_url = require("./shared/url-Doc0XXW_.cjs");
2
+ const require_url = require("./shared/url-CDVgMIq5.cjs");
3
3
  const require_errors = require("./shared/errors-BmP3TKoW.cjs");
4
4
  const require_redact = require("./shared/redact-lAmck1Z6.cjs");
5
- const require_render = require("./shared/render-DqVzx68r.cjs");
5
+ const require_render = require("./shared/render-BimHjOhw.cjs");
6
6
  const require_options = require("./shared/options-DjEtbVaf.cjs");
7
- const require_events = require("./shared/events-BsRm_Fba.cjs");
7
+ const require_events = require("./shared/events-BB791aPk.cjs");
8
8
  const require_key_value_store = require("./shared/key-value-store-B5GYVEYZ.cjs");
9
9
  const require_storage = require("./shared/storage-CZJZoaz9.cjs");
10
10
  const require_multi_storage = require("./shared/multi-storage-s-fPtjoy.cjs");
@@ -32,12 +32,37 @@ const SignInStatus = Object.freeze({
32
32
  Authenticated: "authenticated",
33
33
  OtpRequired: "otp_required"
34
34
  });
35
+ /**
36
+ * Поле тела запроса по умолчанию для известных провайдеров.
37
+ *
38
+ * Используется, когда провайдера не спрашивали. Ответ `captchaProvider()` важнее этой таблицы.
39
+ */
40
+ const CAPTCHA_FIELDS = Object.freeze({
41
+ [require_url.CaptchaType.Itd]: require_url.CaptchaField.Itd,
42
+ [require_url.CaptchaType.Cloudflare]: require_url.CaptchaField.Cloudflare
43
+ });
44
+ /** Состояния, которые может вернуть проверка QR-входа. */
45
+ const QrLoginStatus = Object.freeze({
46
+ Pending: "pending",
47
+ Scanned: "scanned",
48
+ CaptchaRequired: "captcha_required",
49
+ Authorized: "authorized",
50
+ Rejected: "rejected"
51
+ });
52
+ /** Состояния, приходящие из потокового наблюдения за QR-входом. */
53
+ const QrLoginStreamStatus = Object.freeze({
54
+ Pending: "pending",
55
+ Scanned: "scanned",
56
+ Approved: "approved",
57
+ Rejected: "rejected"
58
+ });
35
59
  function requireString(body, key, message) {
36
60
  const value = require_url.pickString(body, key);
37
61
  if (!value) throw new require_errors.ItdConfigError(message);
38
62
  return value;
39
63
  }
40
64
  const AUTH_CHECK = require_url.passthroughOperation("auth.check");
65
+ const AUTH_CAPTCHA_PROVIDER = require_url.passthroughOperation("auth.captchaProvider");
41
66
  const AUTH_SIGN_UP = require_url.defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
42
67
  const AUTH_SIGN_IN = require_url.defineBuiltInOperation("auth.signIn", (body) => {
43
68
  const accessToken = require_url.pickString(body, "accessToken");
@@ -59,6 +84,84 @@ const AUTH_CHANGE_PASSWORD = require_url.voidOperation("auth.changePassword");
59
84
  const AUTH_SESSIONS = require_url.defineBuiltInOperation("auth.sessions", (body) => require_url.pickArray(body, "sessions"));
60
85
  const AUTH_REVOKE_SESSION = require_url.voidOperation("auth.revokeSession");
61
86
  const AUTH_REVOKE_OTHER_SESSIONS = require_url.voidOperation("auth.revokeOtherSessions");
87
+ const AUTH_QR_START = require_url.passthroughOperation("auth.qrStart");
88
+ const AUTH_QR_CLAIM = require_url.passthroughOperation("auth.qrClaim");
89
+ //#endregion
90
+ //#region src/session/captcha.ts
91
+ function requireCaptchaText(value, field) {
92
+ if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new require_errors.ItdConfigError(`${field} должен быть непустой строкой`);
93
+ }
94
+ /**
95
+ * Проверяет форму блока `auth.captcha`.
96
+ *
97
+ * Отсутствие источника токена ошибкой не считается: до входа по паролю дело может и не
98
+ * дойти. О нехватке сообщит {@link resolveCaptchaBody} в момент входа.
99
+ *
100
+ * @throws {ItdConfigError} при некорректных значениях
101
+ */
102
+ function validateCaptchaOptions(captcha) {
103
+ if (captcha === void 0) return;
104
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("auth.captcha должен быть объектом");
105
+ const { type, token, getToken, field } = captcha;
106
+ requireCaptchaText(type, "auth.captcha.type");
107
+ requireCaptchaText(token, "auth.captcha.token");
108
+ requireCaptchaText(field, "auth.captcha.field");
109
+ if (getToken !== void 0 && typeof getToken !== "function") throw new require_errors.ItdConfigError("auth.captcha.getToken должен быть функцией");
110
+ }
111
+ /** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
112
+ function readCaptchaProvider(body) {
113
+ const config = require_url.isRecord$1(body) ? body : void 0;
114
+ const provider = config?.provider;
115
+ const field = config?.field;
116
+ if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new require_errors.ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
117
+ return {
118
+ provider,
119
+ field
120
+ };
121
+ }
122
+ /** Куда класть токен: настройка, иначе умолчание провайдера. */
123
+ function resolveField(type, configured) {
124
+ const field = configured ?? CAPTCHA_FIELDS[type];
125
+ if (field) return field;
126
+ throw new require_errors.ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте auth.captcha.field либо оставьте auth.captcha.type = ${require_url.CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
127
+ }
128
+ /** Спрашивает свежий токен, затем берёт разовый. */
129
+ async function resolveToken(captcha, type) {
130
+ const token = await captcha.getToken?.(type) || captcha.token;
131
+ if (token) return token;
132
+ throw new require_errors.ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте auth.captcha.getToken.`);
133
+ }
134
+ /**
135
+ * Готовит фрагмент тела запроса с токеном капчи.
136
+ *
137
+ * При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
138
+ * и имя поля приходят оттуда. Явно названный тип решается без этого запроса.
139
+ *
140
+ * @param askProvider как спросить активного провайдера; вызывается, только если это нужно
141
+ * @throws {ItdConfigError} если капча не настроена или токен не получен
142
+ */
143
+ async function resolveCaptchaBody(captcha, askProvider) {
144
+ if (!captcha?.token && !captcha?.getToken) throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи. Передайте auth.captcha.getToken — источник свежего токена — либо разовый auth.captcha.token. В Node токен получает пакет @itd-api/captcha.");
145
+ const choice = captcha.type ?? require_url.CaptchaChoice.Auto;
146
+ if (choice === require_url.CaptchaChoice.Auto) {
147
+ const { provider, field } = await askProvider();
148
+ return { [field]: await resolveToken(captcha, provider) };
149
+ }
150
+ return { [resolveField(choice, captcha.field)]: await resolveToken(captcha, choice) };
151
+ }
152
+ /**
153
+ * Готовит фрагмент тела для разового вызова с готовым доказательством.
154
+ *
155
+ * Поле берётся из самого доказательства либо из умолчания провайдера; запроса к серверу нет.
156
+ *
157
+ * @throws {ItdConfigError} если доказательство неполное
158
+ */
159
+ function captchaBody(captcha) {
160
+ if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("captcha должен быть объектом { type, token }");
161
+ if (typeof captcha.token !== "string" || captcha.token.trim() === "") throw new require_errors.ItdConfigError("captcha.token должен быть непустой строкой");
162
+ if (typeof captcha.type !== "string" || captcha.type.trim() === "") throw new require_errors.ItdConfigError("captcha.type должен быть непустой строкой");
163
+ return { [resolveField(captcha.type, captcha.field)]: captcha.token };
164
+ }
62
165
  //#endregion
63
166
  //#region src/session/jwt.ts
64
167
  /**
@@ -75,38 +178,34 @@ function decodeBase64Url(segment) {
75
178
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
76
179
  return new TextDecoder().decode(bytes);
77
180
  }
78
- /**
79
- * Читает `sub` и `sid` из полезной нагрузки JWT.
80
- *
81
- * Подпись не проверяется — ключа для проверки у клиента нет. Прочитанные значения служат
82
- * лишь метками для разделения локального состояния и не дают доступа: его определяет сервер.
83
- */
84
- function readTokenIdentity(token) {
181
+ /** Читает полезную нагрузку JWT без проверки подписи. */
182
+ function readTokenPayload(token) {
85
183
  try {
86
184
  const payload = token.split(".")[1];
87
- if (!payload) return {};
185
+ if (!payload) return void 0;
88
186
  const parsed = JSON.parse(decodeBase64Url(payload));
89
- if (typeof parsed !== "object" || parsed === null) return {};
90
- const { sub, sid } = parsed;
91
- return {
92
- ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
93
- ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
94
- };
187
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
188
+ return parsed;
95
189
  } catch {
96
- return {};
190
+ return;
97
191
  }
98
192
  }
99
- /**
100
- * Читает `sub` из полезной нагрузки JWT.
101
- *
102
- * @returns идентификатор владельца токена; `undefined`, если токен не JWT, повреждён
103
- * или поля `sub` в нём нет
104
- */
105
- function readTokenSubject(token) {
106
- return readTokenIdentity(token).subject;
193
+ /** Читает используемые клиентом поля JWT за один раз. */
194
+ function readTokenMetadata(token) {
195
+ const payload = readTokenPayload(token);
196
+ if (!payload) return {};
197
+ const { sub, sid, exp } = payload;
198
+ const expiresAt = typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
199
+ return {
200
+ ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
201
+ ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {},
202
+ ...expiresAt !== void 0 && Number.isFinite(expiresAt) ? { expiresAt } : {}
203
+ };
107
204
  }
108
205
  //#endregion
109
206
  //#region src/session/auth.ts
207
+ /** Запас до истечения токена на очередь и выполнение запроса. */
208
+ const ACCESS_TOKEN_REFRESH_LEEWAY_MS = 3e4;
110
209
  /**
111
210
  * Проверяет форму `auth` и сообщает о типичных ошибках понятным текстом.
112
211
  *
@@ -135,11 +234,10 @@ function validateAuth(auth) {
135
234
  return { ...auth };
136
235
  }
137
236
  if ("email" in auth || "password" in auth) {
138
- const { email, password, turnstileToken, getTurnstileToken } = auth;
237
+ const { email, password, captcha } = auth;
139
238
  if (typeof email !== "string" || email.trim() === "") throw new require_errors.ItdConfigError("auth.email должен быть непустой строкой");
140
239
  if (typeof password !== "string" || password === "") throw new require_errors.ItdConfigError("auth.password должен быть непустой строкой");
141
- if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") throw new require_errors.ItdConfigError("auth.getTurnstileToken должен быть функцией");
142
- if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new require_errors.ItdConfigError("auth.turnstileToken должен быть непустой строкой");
240
+ validateCaptchaOptions(captcha);
143
241
  return { ...auth };
144
242
  }
145
243
  throw new require_errors.ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
@@ -186,6 +284,7 @@ function createItdAuth(options, deps) {
186
284
  }
187
285
  /** Пути эндпоинтов авторизации. */
188
286
  const AUTH_PATHS = {
287
+ captchaProvider: "/api/v1/auth/captcha/provider",
189
288
  signUp: "/api/v1/auth/sign-up",
190
289
  signIn: "/api/v1/auth/sign-in",
191
290
  verifyOtp: "/api/v1/auth/verify-otp",
@@ -195,7 +294,10 @@ const AUTH_PATHS = {
195
294
  forgotPassword: "/api/v1/auth/forgot-password",
196
295
  resetPassword: "/api/v1/auth/reset-password",
197
296
  changePassword: "/api/v1/auth/change-password",
198
- sessions: "/api/v1/auth/sessions"
297
+ sessions: "/api/v1/auth/sessions",
298
+ qrStart: "/api/v1/auth/qr/start",
299
+ qrClaim: "/api/v1/auth/qr/claim",
300
+ qrStream: "/api/v1/auth/qr/stream"
199
301
  };
200
302
  /**
201
303
  * Публичный ключ Cloudflare Turnstile платформы итд.com.
@@ -205,14 +307,15 @@ const AUTH_PATHS = {
205
307
  *
206
308
  * Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
207
309
  * Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
208
- * подходит `@itd-api/turnstile`, готовый токен из другого источника или вовсе вход
310
+ * подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
209
311
  * без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
210
312
  *
211
313
  * @example
212
314
  * ```ts
213
315
  * turnstile.render('#captcha', {
214
316
  * sitekey: TURNSTILE_SITE_KEY,
215
- * callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
317
+ * callback: (token) =>
318
+ * itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
216
319
  * });
217
320
  * ```
218
321
  */
@@ -306,7 +409,7 @@ var AuthManager = class {
306
409
  * старте читают хранилище один раз и не заводят каждый свой `deviceId`.
307
410
  */
308
411
  #loading = null;
309
- /** Общий промис обновления: к нему присоединяются все, кто получил 401. */
412
+ /** Текущее обновление: к нему присоединяются запросы той же ревизии. */
310
413
  #refreshing = null;
311
414
  /** Общий промис входа по логину и паролю. */
312
415
  #signingIn = null;
@@ -316,6 +419,7 @@ var AuthManager = class {
316
419
  #externalToken;
317
420
  /** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
318
421
  #externalIdentity;
422
+ #tokenMetadata;
319
423
  /**
320
424
  * Идентификатор устройства.
321
425
  *
@@ -329,9 +433,7 @@ var AuthManager = class {
329
433
  * Счётчик смен владельца авторизации.
330
434
  *
331
435
  * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
332
- * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
333
- * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
334
- * воскресить уже очищенную сессию поверх `signOut`.
436
+ * `setSession`, `setAccessToken`, вход. Refresh сверяет снимок до и после сетевого запроса.
335
437
  */
336
438
  #authEpoch = 0;
337
439
  #externalReadGeneration = 0;
@@ -364,6 +466,7 @@ var AuthManager = class {
364
466
  this.#disposed = true;
365
467
  this.#externalReadGeneration += 1;
366
468
  this.#invalidateInFlight();
469
+ this.#tokenMetadata = void 0;
367
470
  this.#emitter.removeAllListeners();
368
471
  }
369
472
  /**
@@ -424,12 +527,25 @@ var AuthManager = class {
424
527
  return this.#session?.accessToken ?? null;
425
528
  }
426
529
  #identityForToken(accessToken) {
427
- const token = accessToken ? readTokenIdentity(accessToken) : {};
530
+ const token = this.#metadataForToken(accessToken);
428
531
  return {
429
532
  ...token.subject ? { userId: token.subject } : {},
430
533
  ...token.sessionId ? { sessionId: token.sessionId } : {}
431
534
  };
432
535
  }
536
+ #metadataForToken(accessToken) {
537
+ if (!accessToken) {
538
+ this.#tokenMetadata = void 0;
539
+ return {};
540
+ }
541
+ if (this.#tokenMetadata?.accessToken === accessToken) return this.#tokenMetadata.value;
542
+ const value = readTokenMetadata(accessToken);
543
+ this.#tokenMetadata = {
544
+ accessToken,
545
+ value
546
+ };
547
+ return value;
548
+ }
433
549
  async #readExternalToken(getToken) {
434
550
  this.#assertActive();
435
551
  const generation = ++this.#externalReadGeneration;
@@ -476,15 +592,33 @@ var AuthManager = class {
476
592
  if (this.#jar.has("is_auth", this.#config.baseUrl)) return true;
477
593
  return Boolean(this.#session?.refreshToken);
478
594
  }
479
- /**
480
- * Готовит состояние авторизации до входа запроса в очередь. Стадия `auth_preparation`.
481
- *
482
- * Загрузка хранилища, внешний источник токена и отложенный вход асинхронны, поэтому
483
- * обязаны завершиться до захвата слота очереди.
484
- */
595
+ /** Готовит внутреннюю сессию до retry исходного запроса. */
596
+ async preflight(allowRefresh) {
597
+ this.#assertActive();
598
+ let accessToken = (await this.#loadSession())?.accessToken;
599
+ if (!accessToken) {
600
+ const auth = this.#config.auth;
601
+ if (typeof auth === "object" && auth !== null && "email" in auth) accessToken = await this.#signInWithCredentials(auth) ?? void 0;
602
+ }
603
+ if (!accessToken || !allowRefresh || !this.#config.autoRefresh) return;
604
+ const expiresAt = this.#metadataForToken(accessToken).expiresAt;
605
+ if (expiresAt === void 0 || expiresAt > this.#config.clock.now() + ACCESS_TOKEN_REFRESH_LEEWAY_MS || !this.#hasRefreshSession()) return;
606
+ try {
607
+ await this.#refreshDeduplicated({
608
+ epoch: this.#authEpoch,
609
+ accessToken
610
+ });
611
+ } catch (error) {
612
+ this.#emitter.emit("authError", { error });
613
+ throw error;
614
+ }
615
+ }
616
+ /** Перед каждой попыткой перечитывает внешний источник токена. */
485
617
  async prepare() {
486
618
  this.#assertActive();
487
- await this.token();
619
+ if ((await this.#loadSession())?.accessToken) return;
620
+ const auth = this.#config.auth;
621
+ if (typeof auth === "object" && auth !== null && "getToken" in auth) await this.#readExternalToken(() => auth.getToken());
488
622
  }
489
623
  /**
490
624
  * Заголовки уже подготовленной авторизации без чтения storage или вызова внешнего источника.
@@ -599,6 +733,13 @@ var AuthManager = class {
599
733
  }
600
734
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
601
735
  async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
736
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
737
+ }
738
+ /** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
739
+ async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
740
+ return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
741
+ }
742
+ async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
602
743
  this.#assertActive();
603
744
  if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
604
745
  await this.#loadSession();
@@ -608,7 +749,7 @@ var AuthManager = class {
608
749
  this.#invalidateInFlight();
609
750
  this.#transitionAuth(accessToken);
610
751
  const saved = this.#saveSession({
611
- ...this.#session ?? {},
752
+ ...replaceSession ? {} : this.#session ?? {},
612
753
  accessToken,
613
754
  obtainedAt: this.#config.clock.now()
614
755
  });
@@ -631,7 +772,7 @@ var AuthManager = class {
631
772
  async getUserId() {
632
773
  this.#assertActive();
633
774
  const session = await this.#loadSession();
634
- return session?.accessToken ? readTokenSubject(session.accessToken) : void 0;
775
+ return session?.accessToken ? this.#identityForToken(session.accessToken).userId : void 0;
635
776
  }
636
777
  /** Заменяет сессию и связанные с ней cookie целиком. */
637
778
  async setSession(session) {
@@ -734,24 +875,30 @@ var AuthManager = class {
734
875
  this.#persistence = pending.catch(() => {});
735
876
  return pending;
736
877
  }
737
- /**
738
- * Обновление с дедупликацией.
739
- *
740
- * Все, кто пришёл, пока обновление уже идёт, получают его результат, а не запускают своё.
741
- */
742
- #refreshDeduplicated() {
743
- if (this.#refreshing) return this.#refreshing;
744
- const promise = this.#performRefresh().finally(() => {
745
- this.#refreshing = null;
878
+ /** Дедуплицирует refresh одной ревизии; следующая ждёт завершения предыдущей. */
879
+ #refreshDeduplicated(expectation = { epoch: this.#authEpoch }) {
880
+ const current = this.#refreshing;
881
+ if (current) {
882
+ if (current.epoch === expectation.epoch) return current.promise;
883
+ return current.promise.then(() => this.#refreshDeduplicated(expectation), () => this.#refreshDeduplicated(expectation));
884
+ }
885
+ const promise = this.#performRefresh(expectation).finally(() => {
886
+ if (this.#refreshing?.promise === promise) this.#refreshing = null;
746
887
  });
747
- this.#refreshing = promise;
888
+ this.#refreshing = {
889
+ epoch: expectation.epoch,
890
+ promise
891
+ };
748
892
  return promise;
749
893
  }
750
- async #performRefresh() {
894
+ #matchesRefreshExpectation(expectation) {
895
+ return this.#authEpoch === expectation.epoch && (expectation.accessToken === void 0 || this.#session?.accessToken === expectation.accessToken);
896
+ }
897
+ async #performRefresh(expectation) {
751
898
  this.#assertActive();
752
899
  await this.#loadSession();
900
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#currentAccessToken();
753
901
  if (!this.#hasRefreshSession()) return this.#reloginOrNull();
754
- const epoch = this.#authEpoch;
755
902
  const flowCookies = this.#jar.clone();
756
903
  try {
757
904
  const accessToken = await this.#http.execute(AUTH_REFRESH, {
@@ -760,7 +907,7 @@ var AuthManager = class {
760
907
  skipAuthRefresh: true,
761
908
  cookieJar: flowCookies
762
909
  });
763
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
910
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#session?.accessToken ?? null;
764
911
  this.#jar.replaceWith(flowCookies);
765
912
  const rotated = this.#jar.getValue(require_redact.REFRESH_COOKIE, this.#config.baseUrl + require_redact.REFRESH_COOKIE_PATH);
766
913
  this.#transitionAuth(accessToken, false);
@@ -772,10 +919,11 @@ var AuthManager = class {
772
919
  });
773
920
  this.#emitter.emit("tokens", { accessToken });
774
921
  await saved;
922
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
775
923
  return accessToken;
776
924
  } catch (error) {
925
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
777
926
  if (error instanceof require_errors.ItdAuthError) {
778
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
779
927
  this.#invalidateInFlight();
780
928
  const clearedRevision = this.#authEpoch;
781
929
  this.#transitionAuth(void 0);
@@ -816,29 +964,23 @@ var AuthManager = class {
816
964
  this.#signingIn = promise;
817
965
  return promise;
818
966
  }
819
- /**
820
- * Берёт токен капчи для входа.
821
- *
822
- * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
823
- * несколько минут, поэтому при повторном входе через сутки годится только свежий.
824
- */
825
- async #resolveTurnstileToken(credentials) {
826
- if (credentials.getTurnstileToken) {
827
- const token = await credentials.getTurnstileToken();
828
- if (token) return token;
829
- }
830
- if (credentials.turnstileToken) return credentials.turnstileToken;
831
- throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета — TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
967
+ /** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
968
+ async #resolveCaptchaProvider() {
969
+ return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
970
+ path: AUTH_PATHS.captchaProvider,
971
+ skipAuth: true,
972
+ skipAuthRefresh: true
973
+ }));
832
974
  }
833
975
  async #performSignIn(credentials, revision) {
834
- const turnstileToken = await this.#resolveTurnstileToken(credentials);
976
+ const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
835
977
  const flowCookies = new require_redact.CookieJar();
836
978
  const result = await this.#http.execute(AUTH_SIGN_IN, {
837
979
  path: AUTH_PATHS.signIn,
838
980
  body: {
839
981
  email: credentials.email,
840
982
  password: credentials.password,
841
- turnstileToken
983
+ ...captcha
842
984
  },
843
985
  skipAuth: true,
844
986
  skipAuthRefresh: true,
@@ -862,6 +1004,125 @@ var AuthManager = class {
862
1004
  }
863
1005
  };
864
1006
  //#endregion
1007
+ //#region src/session/qr-stream.ts
1008
+ const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
1009
+ function readQrLoginStreamEvent(data) {
1010
+ let value;
1011
+ try {
1012
+ value = JSON.parse(data);
1013
+ } catch {
1014
+ return;
1015
+ }
1016
+ if (!require_url.isRecord$1(value)) return void 0;
1017
+ const event = value;
1018
+ if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
1019
+ return {
1020
+ status: event.status,
1021
+ ...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
1022
+ };
1023
+ }
1024
+ /** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
1025
+ async function consumeQrLoginStream(body, onEvent, signal) {
1026
+ const reader = body.getReader();
1027
+ const decoder = new TextDecoder();
1028
+ let delivery = Promise.resolve();
1029
+ let completed = false;
1030
+ const cancel = () => {
1031
+ reader.cancel(signal?.reason).catch(() => {});
1032
+ };
1033
+ signal?.addEventListener("abort", cancel, { once: true });
1034
+ const parser = require_events.createParser({ onEvent(message) {
1035
+ const event = readQrLoginStreamEvent(message.data);
1036
+ if (!event) return;
1037
+ delivery = delivery.then(() => onEvent(event));
1038
+ } });
1039
+ try {
1040
+ for (;;) {
1041
+ const { done, value } = await reader.read();
1042
+ if (done) break;
1043
+ parser.feed(decoder.decode(value, { stream: true }));
1044
+ await delivery;
1045
+ }
1046
+ parser.feed(decoder.decode());
1047
+ await delivery;
1048
+ if (signal?.aborted) throw signal.reason;
1049
+ completed = true;
1050
+ } finally {
1051
+ signal?.removeEventListener("abort", cancel);
1052
+ if (!completed) await reader.cancel().catch(() => {});
1053
+ reader.releaseLock?.();
1054
+ }
1055
+ }
1056
+ /** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
1057
+ async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
1058
+ connection.assertActive?.("открыть QR-поток");
1059
+ const path = AUTH_PATHS.qrStream;
1060
+ const method = "POST";
1061
+ const url = require_redact.joinUrl(connection.baseUrl, path);
1062
+ const abort = require_url.createRequestAbortScope(signal, connection.signal, 0, connection.clock);
1063
+ try {
1064
+ const headers = await connection.baseHeaders(url);
1065
+ headers.delete("Authorization");
1066
+ headers.delete("Cookie");
1067
+ headers.set("Accept", "text/event-stream");
1068
+ headers.set("Content-Type", "application/json");
1069
+ const cookie = cookies.getHeader(url);
1070
+ if (cookie) headers.set("Cookie", cookie);
1071
+ let response;
1072
+ try {
1073
+ response = await connection.fetch(url, {
1074
+ method,
1075
+ headers,
1076
+ body: JSON.stringify(input),
1077
+ credentials: "include",
1078
+ signal: abort.signal
1079
+ });
1080
+ } catch (error) {
1081
+ const failure = new require_errors.ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
1082
+ method,
1083
+ path,
1084
+ cause: error
1085
+ });
1086
+ throw require_url.requestAbortError(abort, {
1087
+ timeout: 0,
1088
+ method,
1089
+ path
1090
+ }, failure);
1091
+ }
1092
+ cookies.setFromResponse(response.url || url, response);
1093
+ if (!response.ok) {
1094
+ const text = await response.text();
1095
+ let body = text;
1096
+ try {
1097
+ body = JSON.parse(text);
1098
+ } catch {}
1099
+ throw require_url.createApiError({
1100
+ method,
1101
+ path,
1102
+ status: response.status,
1103
+ statusText: response.statusText,
1104
+ headers: response.headers,
1105
+ response,
1106
+ body,
1107
+ now: connection.clock.now()
1108
+ });
1109
+ }
1110
+ if (!response.body) throw new require_errors.ItdNetworkError("Сервер не вернул тело QR-потока", {
1111
+ method,
1112
+ path
1113
+ });
1114
+ await consumeQrLoginStream(response.body, onEvent, abort.signal);
1115
+ } catch (error) {
1116
+ throw require_url.requestAbortError(abort, {
1117
+ timeout: 0,
1118
+ method,
1119
+ path
1120
+ }, error);
1121
+ } finally {
1122
+ abort.cleanup();
1123
+ }
1124
+ }
1125
+ //#endregion
865
1126
  //#region src/resources/auth.ts
866
1127
  /**
867
1128
  * Результат входа.
@@ -876,9 +1137,35 @@ var AuthManager = class {
876
1137
  */
877
1138
  var AuthResource = class extends require_render.BaseResource {
878
1139
  #auth;
1140
+ #connection;
1141
+ #qrFlows = /* @__PURE__ */ new Map();
879
1142
  constructor(http, deps) {
880
1143
  super(http);
881
1144
  this.#auth = deps.auth;
1145
+ this.#connection = deps.connection;
1146
+ }
1147
+ #pruneQrFlows() {
1148
+ const now = this.#connection.clock.now();
1149
+ for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
1150
+ }
1151
+ #qrFlow(qrId) {
1152
+ this.#pruneQrFlows();
1153
+ const existing = this.#qrFlows.get(qrId);
1154
+ if (existing) return existing;
1155
+ const flow = {
1156
+ cookies: this.#auth.createCookieFlow(false),
1157
+ expiresAt: void 0
1158
+ };
1159
+ this.#qrFlows.set(qrId, flow);
1160
+ return flow;
1161
+ }
1162
+ #touchQrFlow(flow, expiresIn) {
1163
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
1164
+ const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
1165
+ flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
1166
+ }
1167
+ #forgetTerminalQrError(qrId, error) {
1168
+ if (require_errors.isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
882
1169
  }
883
1170
  /**
884
1171
  * Проверяет состояние авторизации и возвращает текущего пользователя.
@@ -892,14 +1179,106 @@ var AuthResource = class extends require_render.BaseResource {
892
1179
  });
893
1180
  }
894
1181
  /**
1182
+ * Узнаёт активного провайдера капчи и поле токена.
1183
+ *
1184
+ * Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
1185
+ * Автоматическому входу этот вызов не нужен — клиент делает его сам.
1186
+ */
1187
+ captchaProvider(options = {}) {
1188
+ return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
1189
+ path: AUTH_PATHS.captchaProvider,
1190
+ skipAuth: true,
1191
+ skipAuthRefresh: true,
1192
+ ...options
1193
+ });
1194
+ }
1195
+ /** Создаёт короткоживущую сессию QR-входа. */
1196
+ async startQrLogin(options = {}) {
1197
+ const cookies = this.#auth.createCookieFlow(false);
1198
+ const result = await this.http.execute(AUTH_QR_START, {
1199
+ path: AUTH_PATHS.qrStart,
1200
+ skipAuth: true,
1201
+ skipAuthRefresh: true,
1202
+ cookieJar: cookies,
1203
+ ...options
1204
+ });
1205
+ if (typeof result.qrId === "string" && result.qrId !== "") {
1206
+ const flow = {
1207
+ cookies,
1208
+ expiresAt: void 0
1209
+ };
1210
+ this.#touchQrFlow(flow, result.expiresIn);
1211
+ this.#qrFlows.set(result.qrId, flow);
1212
+ }
1213
+ return result;
1214
+ }
1215
+ /**
1216
+ * Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
1217
+ *
1218
+ * До подтверждения возвращает промежуточный статус. Если сервер потребовал капчу,
1219
+ * повторите вызов с `captcha` — тип берётся из {@link captchaProvider}.
1220
+ * Полученный `accessToken` автоматически сохраняется в клиенте.
1221
+ */
1222
+ async claimQrLogin(input, options = {}) {
1223
+ const revision = this.#auth.revision();
1224
+ const flow = this.#qrFlow(input.qrId);
1225
+ try {
1226
+ const { captcha, ...rest } = input;
1227
+ const result = await this.http.execute(AUTH_QR_CLAIM, {
1228
+ path: AUTH_PATHS.qrClaim,
1229
+ body: {
1230
+ ...rest,
1231
+ ...captcha ? captchaBody(captcha) : {}
1232
+ },
1233
+ skipAuth: true,
1234
+ skipAuthRefresh: true,
1235
+ cookieJar: flow.cookies,
1236
+ ...options
1237
+ });
1238
+ this.#touchQrFlow(flow, result.expiresIn);
1239
+ if (result.status === QrLoginStatus.Authorized) {
1240
+ await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
1241
+ this.#qrFlows.delete(input.qrId);
1242
+ } else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
1243
+ return result;
1244
+ } catch (error) {
1245
+ this.#forgetTerminalQrError(input.qrId, error);
1246
+ throw error;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
1251
+ *
1252
+ * Событие `approved` означает, что подтверждение завершено: после него вызовите
1253
+ * {@link claimQrLogin}, чтобы получить и сохранить access token.
1254
+ */
1255
+ async streamQrLogin(input, onEvent, options = {}) {
1256
+ if (typeof onEvent !== "function") throw new require_errors.ItdConfigError("onEvent QR-потока должен быть функцией");
1257
+ const flow = this.#qrFlow(input.qrId);
1258
+ try {
1259
+ await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
1260
+ this.#touchQrFlow(flow, event.expiresIn);
1261
+ if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
1262
+ await onEvent(event);
1263
+ }, options.signal);
1264
+ } catch (error) {
1265
+ this.#forgetTerminalQrError(input.qrId, error);
1266
+ throw error;
1267
+ }
1268
+ }
1269
+ /**
895
1270
  * Регистрирует аккаунт и запускает подтверждение по коду.
896
1271
  *
897
1272
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
898
1273
  */
899
1274
  signUp(credentials, options = {}) {
1275
+ const { captcha, ...rest } = credentials;
900
1276
  return this.http.execute(AUTH_SIGN_UP, {
901
1277
  path: AUTH_PATHS.signUp,
902
- body: credentials,
1278
+ body: {
1279
+ ...rest,
1280
+ ...captchaBody(captcha)
1281
+ },
903
1282
  skipAuth: true,
904
1283
  skipAuthRefresh: true,
905
1284
  ...options
@@ -916,17 +1295,21 @@ var AuthResource = class extends require_render.BaseResource {
916
1295
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
917
1296
  */
918
1297
  async signIn(credentials, options = {}) {
1298
+ const { captcha, ...rest } = credentials;
919
1299
  const revision = this.#auth.revision();
920
1300
  const cookies = this.#auth.createCookieFlow(false);
921
1301
  const result = await this.http.execute(AUTH_SIGN_IN, {
922
1302
  path: AUTH_PATHS.signIn,
923
- body: credentials,
1303
+ body: {
1304
+ ...rest,
1305
+ ...captchaBody(captcha)
1306
+ },
924
1307
  skipAuth: true,
925
1308
  skipAuthRefresh: true,
926
1309
  cookieJar: cookies,
927
1310
  ...options
928
1311
  });
929
- if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken, revision, cookies);
1312
+ if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
930
1313
  else this.#auth.commitCookieFlow(cookies, revision);
931
1314
  return result;
932
1315
  }
@@ -946,7 +1329,7 @@ var AuthResource = class extends require_render.BaseResource {
946
1329
  cookieJar: cookies,
947
1330
  ...options
948
1331
  });
949
- await this.#auth.setAccessToken(accessToken, revision, cookies);
1332
+ await this.#auth.commitAuthFlow(accessToken, cookies, revision);
950
1333
  return accessToken;
951
1334
  }
952
1335
  /** Отправляет код подтверждения повторно. */
@@ -1051,7 +1434,10 @@ var AuthResource = class extends require_render.BaseResource {
1051
1434
  forgotPassword(input, options = {}) {
1052
1435
  return this.http.execute(AUTH_FORGOT_PASSWORD, {
1053
1436
  path: AUTH_PATHS.forgotPassword,
1054
- body: input,
1437
+ body: {
1438
+ email: input.email,
1439
+ ...captchaBody(input.captcha)
1440
+ },
1055
1441
  skipAuth: true,
1056
1442
  skipAuthRefresh: true,
1057
1443
  ...options
@@ -1082,23 +1468,21 @@ var AuthResource = class extends require_render.BaseResource {
1082
1468
  * ```ts
1083
1469
  * await itd.auth.resetPasswordWithOtp({
1084
1470
  * email,
1085
- * turnstileToken,
1471
+ * captcha: { type: provider, token, field },
1086
1472
  * newPassword,
1087
1473
  * getOtp: () => rl.question('Код из письма: '),
1088
1474
  * });
1089
1475
  * ```
1090
1476
  */
1091
1477
  async resetPasswordWithOtp(input, options = {}) {
1092
- const flowToken = await this.forgotPassword({
1093
- email: input.email,
1094
- turnstileToken: input.turnstileToken
1095
- }, options);
1096
- const otp = await input.getOtp();
1478
+ const { getOtp, newPassword, ...request } = input;
1479
+ const flowToken = await this.forgotPassword(request, options);
1480
+ const otp = await getOtp();
1097
1481
  await this.resetPassword({
1098
1482
  email: input.email,
1099
1483
  otp,
1100
1484
  flowToken,
1101
- newPassword: input.newPassword
1485
+ newPassword
1102
1486
  }, options);
1103
1487
  }
1104
1488
  /**
@@ -1187,8 +1571,8 @@ function createManagedClient(options, internals) {
1187
1571
  * const itd = new ItdClient({
1188
1572
  * // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
1189
1573
  * // а истёкший продлевается сам. Здесь он нужен на первый запуск.
1190
- * // Вход по паролю требует токена капчи — см. AuthInput и TURNSTILE_SITE_KEY.
1191
- * auth: { email, password, getTurnstileToken },
1574
+ * // Вход по паролю требует токена капчи — см. CaptchaOptions.
1575
+ * auth: { email, password, captcha: createCaptchaSolver() },
1192
1576
  * storage: new FileTokenStorage('./.itd-session.json'),
1193
1577
  * rateLimit: { concurrency: 4, rps: 8 },
1194
1578
  * });
@@ -1209,7 +1593,10 @@ var ItdClient = class ItdClient {
1209
1593
  #auth;
1210
1594
  /** Авторизация, сессии и пароли. */
1211
1595
  get auth() {
1212
- this.#auth ??= new AuthResource(this.#runtime.http, { auth: this.#runtime.auth });
1596
+ this.#auth ??= new AuthResource(this.#runtime.http, {
1597
+ auth: this.#runtime.auth,
1598
+ connection: this.#runtime.connection()
1599
+ });
1213
1600
  return this.#auth;
1214
1601
  }
1215
1602
  /** Профили, подписки, блокировки, приватность. */
@@ -1706,7 +2093,9 @@ function assertAccountsActive(accounts, action) {
1706
2093
  * await accounts.restore();
1707
2094
  *
1708
2095
  * if (!accounts.has('kiow')) {
1709
- * accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
2096
+ * accounts.addAccount('kiow', {
2097
+ * auth: { email, password, captcha: createCaptchaSolver() },
2098
+ * });
1710
2099
  * }
1711
2100
  *
1712
2101
  * await accounts.account('kiow').posts.create({ content: 'привет' });
@@ -1785,7 +2174,9 @@ var ItdAccounts = class ItdAccounts {
1785
2174
  *
1786
2175
  * @example
1787
2176
  * ```ts
1788
- * accounts.addAccount('bot', { auth: { email, password, getTurnstileToken } });
2177
+ * accounts.addAccount('bot', {
2178
+ * auth: { email, password, captcha: createCaptchaSolver() },
2179
+ * });
1789
2180
  * accounts.addAccount('reader', { auth: '<accessToken>' });
1790
2181
  * accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
1791
2182
  * ```
@@ -2123,6 +2514,10 @@ exports.AUDIO_MIME_TYPES = require_render.AUDIO_MIME_TYPES;
2123
2514
  exports.AccessType = require_url.AccessType;
2124
2515
  exports.AttachmentType = require_url.AttachmentType;
2125
2516
  exports.BUCKET_LIMITS = require_url.BUCKET_LIMITS;
2517
+ exports.CAPTCHA_FIELDS = CAPTCHA_FIELDS;
2518
+ exports.CaptchaChoice = require_url.CaptchaChoice;
2519
+ exports.CaptchaField = require_url.CaptchaField;
2520
+ exports.CaptchaType = require_url.CaptchaType;
2126
2521
  exports.CommentSort = require_url.CommentSort;
2127
2522
  exports.DEFAULT_BASE_URL = require_url.DEFAULT_BASE_URL;
2128
2523
  exports.DEFAULT_RATE_LIMIT_BUCKET = require_url.DEFAULT_RATE_LIMIT_BUCKET;
@@ -2172,6 +2567,8 @@ exports.NotificationUpdateType = require_events.NotificationUpdateType;
2172
2567
  exports.OPERATIONS = require_url.OPERATIONS;
2173
2568
  exports.PaginationMode = require_render.PaginationMode;
2174
2569
  exports.Paginator = require_render.Paginator;
2570
+ exports.QrLoginStatus = QrLoginStatus;
2571
+ exports.QrLoginStreamStatus = QrLoginStreamStatus;
2175
2572
  exports.RateLimitPacing = require_url.RateLimitPacing;
2176
2573
  exports.ReportReason = require_url.ReportReason;
2177
2574
  exports.ReportTargetType = require_url.ReportTargetType;