itd-api 0.0.2 → 0.0.4

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.
@@ -14,6 +14,30 @@ function resolveInput(input, factory, validate) {
14
14
 
15
15
  // src/core/errors.ts
16
16
  var ITD_ERROR = /* @__PURE__ */ Symbol.for("itd.error");
17
+ var ItdErrorKind = Object.freeze({
18
+ /** Сервер ответил статусом ≥ 400. */
19
+ Api: "api",
20
+ /** Запрос не дошёл до сервера. */
21
+ Network: "network",
22
+ /** Истёк таймаут запроса. */
23
+ Timeout: "timeout",
24
+ /** Запрос отменён через `AbortSignal`. */
25
+ Abort: "abort",
26
+ /** Некорректная конфигурация или аргументы — обнаружено до обращения к сети. */
27
+ Config: "config"
28
+ });
29
+ var ItdApiErrorKind = Object.freeze({
30
+ /** Ни одна из специализаций не подошла. */
31
+ Generic: "generic",
32
+ Validation: "validation",
33
+ Auth: "auth",
34
+ Forbidden: "forbidden",
35
+ NotFound: "not_found",
36
+ Conflict: "conflict",
37
+ RateLimit: "rate_limit",
38
+ PhoneVerification: "phone_verification",
39
+ Server: "server"
40
+ });
17
41
  var ItdError = class extends Error {
18
42
  /** @internal */
19
43
  [ITD_ERROR] = true;
@@ -26,6 +50,13 @@ var ItdError = class extends Error {
26
50
  }
27
51
  };
28
52
  var ItdApiError = class extends ItdError {
53
+ /**
54
+ * Разновидность ошибки: та же информация, что и класс, но пригодная для сравнения.
55
+ *
56
+ * Позволяет разбирать ошибку через `switch`, а проверкам вроде {@link isItdAuthError} —
57
+ * работать даже когда в проекте оказались две копии библиотеки.
58
+ */
59
+ apiKind;
29
60
  /** HTTP-статус ответа. */
30
61
  status;
31
62
  /** Строковый код ошибки, например `VALIDATION_ERROR`. */
@@ -56,9 +87,13 @@ var ItdApiError = class extends ItdError {
56
87
  rateLimit;
57
88
  /** Сколько запросов осталось в окне — заголовок `x-ratelimit-remaining`. */
58
89
  rateLimitRemaining;
59
- constructor(init) {
60
- super("api", init.message);
90
+ /**
91
+ * @param apiKind разновидность; подставляется подклассами, снаружи задавать не нужно
92
+ */
93
+ constructor(init, apiKind = ItdApiErrorKind.Generic) {
94
+ super(ItdErrorKind.Api, init.message);
61
95
  this.name = "ItdApiError";
96
+ this.apiKind = apiKind;
62
97
  this.status = init.status;
63
98
  this.code = init.code;
64
99
  this.detail = init.detail;
@@ -91,37 +126,37 @@ var ItdApiError = class extends ItdError {
91
126
  };
92
127
  var ItdValidationError = class extends ItdApiError {
93
128
  constructor(init) {
94
- super(init);
129
+ super(init, ItdApiErrorKind.Validation);
95
130
  this.name = "ItdValidationError";
96
131
  }
97
132
  };
98
133
  var ItdAuthError = class extends ItdApiError {
99
134
  constructor(init) {
100
- super(init);
135
+ super(init, ItdApiErrorKind.Auth);
101
136
  this.name = "ItdAuthError";
102
137
  }
103
138
  };
104
139
  var ItdForbiddenError = class extends ItdApiError {
105
140
  constructor(init) {
106
- super(init);
141
+ super(init, ItdApiErrorKind.Forbidden);
107
142
  this.name = "ItdForbiddenError";
108
143
  }
109
144
  };
110
145
  var ItdNotFoundError = class extends ItdApiError {
111
146
  constructor(init) {
112
- super(init);
147
+ super(init, ItdApiErrorKind.NotFound);
113
148
  this.name = "ItdNotFoundError";
114
149
  }
115
150
  };
116
151
  var ItdConflictError = class extends ItdApiError {
117
152
  constructor(init) {
118
- super(init);
153
+ super(init, ItdApiErrorKind.Conflict);
119
154
  this.name = "ItdConflictError";
120
155
  }
121
156
  };
122
157
  var ItdRateLimitError = class extends ItdApiError {
123
158
  constructor(init) {
124
- super(init);
159
+ super(init, ItdApiErrorKind.RateLimit);
125
160
  this.name = "ItdRateLimitError";
126
161
  }
127
162
  };
@@ -129,14 +164,14 @@ var ItdPhoneVerificationError = class extends ItdApiError {
129
164
  /** Ссылка на бота подтверждения, если удалось определить идентификатор пользователя. */
130
165
  verificationUrl;
131
166
  constructor(init) {
132
- super(init);
167
+ super(init, ItdApiErrorKind.PhoneVerification);
133
168
  this.name = "ItdPhoneVerificationError";
134
169
  this.verificationUrl = init.userId ? `https://t.me/itd_verification_bot?start=${encodeURIComponent(init.userId)}` : void 0;
135
170
  }
136
171
  };
137
172
  var ItdServerError = class extends ItdApiError {
138
173
  constructor(init) {
139
- super(init);
174
+ super(init, ItdApiErrorKind.Server);
140
175
  this.name = "ItdServerError";
141
176
  }
142
177
  };
@@ -146,7 +181,7 @@ var ItdNetworkError = class extends ItdError {
146
181
  /** Путь запроса без базового URL. */
147
182
  path;
148
183
  constructor(message, init) {
149
- super("network", message, { cause: init.cause });
184
+ super(ItdErrorKind.Network, message, { cause: init.cause });
150
185
  this.name = "ItdNetworkError";
151
186
  this.method = init.method;
152
187
  this.path = init.path;
@@ -160,7 +195,10 @@ var ItdTimeoutError = class extends ItdError {
160
195
  /** Путь запроса без базового URL. */
161
196
  path;
162
197
  constructor(init) {
163
- super("timeout", `\u0417\u0430\u043F\u0440\u043E\u0441 ${init.method} ${init.path} \u043F\u0440\u0435\u0432\u044B\u0441\u0438\u043B \u0442\u0430\u0439\u043C\u0430\u0443\u0442 ${init.timeout} \u043C\u0441`);
198
+ super(
199
+ ItdErrorKind.Timeout,
200
+ `\u0417\u0430\u043F\u0440\u043E\u0441 ${init.method} ${init.path} \u043F\u0440\u0435\u0432\u044B\u0441\u0438\u043B \u0442\u0430\u0439\u043C\u0430\u0443\u0442 ${init.timeout} \u043C\u0441`
201
+ );
164
202
  this.name = "ItdTimeoutError";
165
203
  this.timeout = init.timeout;
166
204
  this.method = init.method;
@@ -169,13 +207,13 @@ var ItdTimeoutError = class extends ItdError {
169
207
  };
170
208
  var ItdAbortError = class extends ItdError {
171
209
  constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
172
- super("abort", message);
210
+ super(ItdErrorKind.Abort, message);
173
211
  this.name = "ItdAbortError";
174
212
  }
175
213
  };
176
214
  var ItdConfigError = class extends ItdError {
177
215
  constructor(message) {
178
- super("config", message);
216
+ super(ItdErrorKind.Config, message);
179
217
  this.name = "ItdConfigError";
180
218
  }
181
219
  };
@@ -183,16 +221,34 @@ function isItdError(value) {
183
221
  return typeof value === "object" && value !== null && ITD_ERROR in value;
184
222
  }
185
223
  function isItdApiError(value) {
186
- return isItdError(value) && value.kind === "api";
224
+ return isItdError(value) && value.kind === ItdErrorKind.Api;
225
+ }
226
+ function hasApiKind(value, kind) {
227
+ return isItdApiError(value) && value.apiKind === kind;
187
228
  }
188
229
  function isItdValidationError(value) {
189
- return isItdApiError(value) && value instanceof ItdValidationError;
230
+ return hasApiKind(value, ItdApiErrorKind.Validation);
190
231
  }
191
232
  function isItdAuthError(value) {
192
- return isItdApiError(value) && value instanceof ItdAuthError;
233
+ return hasApiKind(value, ItdApiErrorKind.Auth);
234
+ }
235
+ function isItdForbiddenError(value) {
236
+ return hasApiKind(value, ItdApiErrorKind.Forbidden);
237
+ }
238
+ function isItdNotFoundError(value) {
239
+ return hasApiKind(value, ItdApiErrorKind.NotFound);
240
+ }
241
+ function isItdConflictError(value) {
242
+ return hasApiKind(value, ItdApiErrorKind.Conflict);
193
243
  }
194
244
  function isItdRateLimitError(value) {
195
- return isItdApiError(value) && value instanceof ItdRateLimitError;
245
+ return hasApiKind(value, ItdApiErrorKind.RateLimit);
246
+ }
247
+ function isItdPhoneVerificationError(value) {
248
+ return hasApiKind(value, ItdApiErrorKind.PhoneVerification);
249
+ }
250
+ function isItdServerError(value) {
251
+ return hasApiKind(value, ItdApiErrorKind.Server);
196
252
  }
197
253
 
198
254
  // src/builders/comment.ts
@@ -586,8 +642,15 @@ var ItdErrorCode = Object.freeze({
586
642
  BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
587
643
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
588
644
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
645
+ /** Сервер отвечает так на `404`, `ENTITY_NOT_FOUND` в этом случае не приходит. */
646
+ NOT_FOUND: "NOT_FOUND",
647
+ /** На практике не приходит: вместо него сервер шлёт `TURNSTILE_VERIFICATION_FAILED`. */
589
648
  CAPTCHA_FAILED: "CAPTCHA_FAILED",
649
+ /** Капча не пройдена: токен Turnstile недействителен, просрочен или уже использован. */
650
+ TURNSTILE_VERIFICATION_FAILED: "TURNSTILE_VERIFICATION_FAILED",
590
651
  OTP_INVALID: "OTP_INVALID",
652
+ /** `flowToken` неизвестен или просрочен — поток подтверждения нужно начинать заново. */
653
+ INVALID_FLOW_TOKEN: "INVALID_FLOW_TOKEN",
591
654
  ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
592
655
  ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
593
656
  ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
@@ -596,6 +659,10 @@ var ItdErrorCode = Object.freeze({
596
659
  SESSION_EXPIRED: "SESSION_EXPIRED",
597
660
  SESSION_REVOKED: "SESSION_REVOKED",
598
661
  SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
662
+ /** Запрос обновления пришёл без cookie `refresh_token` — продлевать нечего. */
663
+ REFRESH_TOKEN_MISSING: "REFRESH_TOKEN_MISSING",
664
+ /** Cookie `refresh_token` есть, но сессии за ней уже нет: отозвана или истекла. */
665
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
599
666
  MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
600
667
  PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
601
668
  PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
@@ -611,13 +678,14 @@ var ItdErrorCode = Object.freeze({
611
678
 
612
679
  // src/builders/report.ts
613
680
  var REASONS = new Set(Object.values(ReportReason));
681
+ var TARGET_TYPES = new Set(Object.values(ReportTargetType));
614
682
  function validateReport(input) {
615
683
  if (!input?.targetId || typeof input.targetId !== "string") {
616
684
  throw new ItdConfigError("\u0416\u0430\u043B\u043E\u0431\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043E\u0431\u044A\u0435\u043A\u0442\u0430 (targetId)");
617
685
  }
618
- if (input.targetType !== "post" && input.targetType !== "comment" && input.targetType !== "user") {
686
+ if (!TARGET_TYPES.has(input.targetType)) {
619
687
  throw new ItdConfigError(
620
- `targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C 'post', 'comment' \u0438\u043B\u0438 'user', \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(input.targetType)}`
688
+ `targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${[...TARGET_TYPES].join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(input.targetType)}`
621
689
  );
622
690
  }
623
691
  if (!REASONS.has(input.reason)) {
@@ -655,11 +723,11 @@ function start(targetType, targetId) {
655
723
  }
656
724
  var report = Object.freeze({
657
725
  /** Жалоба на пост. */
658
- post: (postId) => start("post", postId),
726
+ post: (postId) => start(ReportTargetType.Post, postId),
659
727
  /** Жалоба на комментарий. */
660
- comment: (commentId) => start("comment", commentId),
728
+ comment: (commentId) => start(ReportTargetType.Comment, commentId),
661
729
  /** Жалоба на пользователя. */
662
- user: (userId) => start("user", userId)
730
+ user: (userId) => start(ReportTargetType.User, userId)
663
731
  });
664
732
  function resolveReport(input) {
665
733
  return resolveInput(input, () => new ReportBuilder({}), validateReport);
@@ -859,6 +927,8 @@ parseSetCookie.splitCookiesString = splitCookiesString;
859
927
 
860
928
  // src/core/cookies.ts
861
929
  var AUTH_FLAG_COOKIE = "is_auth";
930
+ var REFRESH_COOKIE = "refresh_token";
931
+ var REFRESH_COOKIE_PATH = "/api/v1/auth";
862
932
  var SERIALIZED_SEPARATOR = " ";
863
933
  function originOf(url) {
864
934
  try {
@@ -898,6 +968,15 @@ var CookieJar = class {
898
968
  const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
899
969
  if (raw.length > 0) this.setFromStrings(url, raw);
900
970
  }
971
+ /**
972
+ * Кладёт cookie напрямую, минуя `Set-Cookie`.
973
+ *
974
+ * Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
975
+ * его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
976
+ */
977
+ set(url, name, value, path = "/") {
978
+ this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
979
+ }
901
980
  /** Сохраняет cookie из готовых строк `Set-Cookie`. */
902
981
  setFromStrings(url, setCookieStrings) {
903
982
  const origin = originOf(url);
@@ -931,6 +1010,23 @@ var CookieJar = class {
931
1010
  if (cookies.length === 0) return void 0;
932
1011
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
933
1012
  }
1013
+ /**
1014
+ * Значение действующей cookie.
1015
+ *
1016
+ * Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
1017
+ * продлении сессии, и сохранять надо именно новое значение.
1018
+ *
1019
+ * @param url если указан, учитываются origin, путь и флаг `Secure`
1020
+ */
1021
+ getValue(name, url) {
1022
+ if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
1023
+ const now = Date.now();
1024
+ for (const jar of this.#byOrigin.values()) {
1025
+ const cookie = jar.get(name);
1026
+ if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
1027
+ }
1028
+ return void 0;
1029
+ }
934
1030
  /**
935
1031
  * Есть ли действующая cookie с таким именем.
936
1032
  *
@@ -1059,11 +1155,87 @@ var Emitter = class {
1059
1155
  }
1060
1156
  };
1061
1157
 
1158
+ // src/core/runtime.ts
1159
+ var RuntimeMode = Object.freeze({
1160
+ /** Определяется по среде исполнения. Значение по умолчанию. */
1161
+ Auto: "auto",
1162
+ /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
1163
+ Browser: "browser",
1164
+ /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
1165
+ Server: "server"
1166
+ });
1167
+ var DetectedRuntime = Object.freeze({
1168
+ Browser: "browser",
1169
+ /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
1170
+ ReactNative: "react-native",
1171
+ Server: "server"
1172
+ });
1173
+ function detectRuntime() {
1174
+ const nav = globalThis.navigator;
1175
+ if (nav?.product === "ReactNative") return DetectedRuntime.ReactNative;
1176
+ if (typeof document !== "undefined") return DetectedRuntime.Browser;
1177
+ return DetectedRuntime.Server;
1178
+ }
1179
+ function shouldUseCookieJar(mode) {
1180
+ if (mode === RuntimeMode.Browser) return false;
1181
+ if (mode === RuntimeMode.Server) return true;
1182
+ return detectRuntime() === DetectedRuntime.Server;
1183
+ }
1184
+ function shouldSendCredentials(mode) {
1185
+ if (mode === RuntimeMode.Browser) return true;
1186
+ if (mode === RuntimeMode.Server) return false;
1187
+ return detectRuntime() === DetectedRuntime.Browser;
1188
+ }
1189
+ function resolveFetch(custom) {
1190
+ if (custom) return custom;
1191
+ if (typeof globalThis.fetch === "function") {
1192
+ return globalThis.fetch.bind(globalThis);
1193
+ }
1194
+ throw new ItdConfigError(
1195
+ "\u0412 \u044D\u0442\u043E\u0439 \u0441\u0440\u0435\u0434\u0435 \u043D\u0435\u0442 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E fetch. \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u0435\u0441\u044C \u0434\u043E Node 18+ \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043E\u043F\u0446\u0438\u044E fetch."
1196
+ );
1197
+ }
1198
+ function isBlob(value) {
1199
+ return typeof Blob !== "undefined" && value instanceof Blob;
1200
+ }
1201
+ function isFile(value) {
1202
+ return typeof File !== "undefined" && value instanceof File;
1203
+ }
1204
+ function supportsStreamingBody() {
1205
+ return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1206
+ }
1207
+ function createDeviceId() {
1208
+ const webCrypto = globalThis.crypto;
1209
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
1210
+ const bytes = new Uint8Array(16);
1211
+ if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
1212
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1213
+ bytes[6] = bytes[6] & 15 | 64;
1214
+ bytes[8] = bytes[8] & 63 | 128;
1215
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1216
+ return [
1217
+ hex.slice(0, 8),
1218
+ hex.slice(8, 12),
1219
+ hex.slice(12, 16),
1220
+ hex.slice(16, 20),
1221
+ hex.slice(20, 32)
1222
+ ].join("-");
1223
+ }
1224
+ function hasLocalStorage() {
1225
+ try {
1226
+ return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1227
+ } catch {
1228
+ return false;
1229
+ }
1230
+ }
1231
+
1062
1232
  // src/core/auth.ts
1063
1233
  var AUTH_PATHS = {
1064
1234
  signIn: "/api/v1/auth/sign-in",
1065
1235
  refresh: "/api/v1/auth/refresh"
1066
1236
  };
1237
+ var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1238
+ var DEVICE_ID_HEADER = "X-Device-Id";
1067
1239
  function readAccessToken(payload) {
1068
1240
  if (typeof payload !== "object" || payload === null) return void 0;
1069
1241
  const token = payload.accessToken;
@@ -1080,6 +1252,13 @@ var AuthManager = class {
1080
1252
  #refreshing = null;
1081
1253
  /** Общий промис входа по логину и паролю. */
1082
1254
  #signingIn = null;
1255
+ /**
1256
+ * Идентификатор устройства.
1257
+ *
1258
+ * Держится отдельно от сессии намеренно: выход из аккаунта не меняет устройство,
1259
+ * поэтому `clear()` его не трогает.
1260
+ */
1261
+ #deviceId;
1083
1262
  constructor(config, http, jar) {
1084
1263
  this.#config = config;
1085
1264
  this.#http = http;
@@ -1096,11 +1275,19 @@ var AuthManager = class {
1096
1275
  /**
1097
1276
  * Есть ли признак живой refresh-сессии.
1098
1277
  *
1099
- * Сайт итд.com ставит рядом с refresh-токеном незакрытую cookie `is_auth` — по ней клиент
1100
- * понимает, что обновление вообще имеет смысл, и не дёргает API у анонимов.
1278
+ * Рядом с refresh-токеном сервер ставит незакрытую cookie `is_auth` — по ней видно,
1279
+ * что продлевать сессию вообще есть смысл, и API не дёргается у анонимов.
1101
1280
  * В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
1281
+ *
1282
+ * Асинхронный, потому что признак может лежать в {@link TokenStorage}: до чтения оттуда
1283
+ * ответ был бы `false` даже при полностью рабочей сохранённой сессии.
1102
1284
  */
1103
- hasRefreshSession() {
1285
+ async hasRefreshSession() {
1286
+ await this.#loadSession();
1287
+ return this.#hasRefreshSession();
1288
+ }
1289
+ /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1290
+ #hasRefreshSession() {
1104
1291
  if (!this.#config.useCookieJar) return true;
1105
1292
  if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1106
1293
  return Boolean(this.#session?.refreshToken);
@@ -1110,6 +1297,23 @@ var AuthManager = class {
1110
1297
  const token = await this.getAccessToken();
1111
1298
  return token ? { Authorization: `Bearer ${token}` } : {};
1112
1299
  }
1300
+ /**
1301
+ * Идентификатор устройства для заголовка `X-Device-Id`.
1302
+ *
1303
+ * Заводится один раз и сохраняется в сессии, чтобы пережить перезапуск процесса:
1304
+ * сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
1305
+ * по новой сессии на каждый старт.
1306
+ */
1307
+ async getDeviceId() {
1308
+ if (this.#deviceId) return this.#deviceId;
1309
+ const session = await this.#loadSession();
1310
+ const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
1311
+ this.#deviceId = deviceId;
1312
+ if (session?.deviceId !== deviceId) {
1313
+ await this.#saveSession({ ...session ?? {}, deviceId });
1314
+ }
1315
+ return deviceId;
1316
+ }
1113
1317
  /**
1114
1318
  * Текущий токен доступа.
1115
1319
  *
@@ -1125,7 +1329,7 @@ var AuthManager = class {
1125
1329
  return await auth.getToken() ?? null;
1126
1330
  }
1127
1331
  if (typeof auth === "object" && "email" in auth) {
1128
- return this.#signInWithCredentials(auth.email, auth.password);
1332
+ return this.#signInWithCredentials(auth);
1129
1333
  }
1130
1334
  return null;
1131
1335
  }
@@ -1145,12 +1349,19 @@ var AuthManager = class {
1145
1349
  return false;
1146
1350
  }
1147
1351
  }
1148
- /** Ошибка «сессию продлить нечем» — одна и та же для `refresh()` и для реакции на 401. */
1352
+ /**
1353
+ * Ошибка «сессию продлить нечем».
1354
+ *
1355
+ * Возникает, только когда обновление даже не начиналось: нет ни cookie `is_auth`,
1356
+ * ни refresh-токена. Если сервер ответил отказом, наружу уходит **его** ошибка —
1357
+ * подменять её этой значило бы прятать причину (`REFRESH_TOKEN_MISSING`,
1358
+ * `SESSION_NOT_FOUND`, `SESSION_REVOKED` — разные поводы и разные действия).
1359
+ */
1149
1360
  #noRefreshSessionError() {
1150
1361
  return new ItdAuthError({
1151
1362
  status: 401,
1152
1363
  code: "SESSION_EXPIRED",
1153
- message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044E\u0449\u0435\u0433\u043E refresh-\u0442\u043E\u043A\u0435\u043D\u0430",
1364
+ message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \u043D\u0438 cookie is_auth, \u043D\u0438 refresh-\u0442\u043E\u043A\u0435\u043D\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0437\u0430\u043D\u043E\u0432\u043E \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 refreshToken \u0432 auth.",
1154
1365
  method: "POST",
1155
1366
  path: AUTH_PATHS.refresh,
1156
1367
  raw: void 0
@@ -1180,13 +1391,21 @@ var AuthManager = class {
1180
1391
  /** Заменяет сессию целиком. */
1181
1392
  async setSession(session) {
1182
1393
  this.#jar.deserialize(session.cookies);
1394
+ this.#deviceId ??= session.deviceId;
1183
1395
  await this.#saveSession(session);
1396
+ this.#seedRefreshCookie();
1184
1397
  }
1185
- /** Забывает сессию и cookie. Сетевой запрос не выполняется. */
1398
+ /**
1399
+ * Забывает сессию и cookie. Сетевой запрос не выполняется.
1400
+ *
1401
+ * Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
1402
+ * новую запись в списке сессий.
1403
+ */
1186
1404
  async clear() {
1187
1405
  this.#session = null;
1188
1406
  this.#jar.clear();
1189
1407
  await this.#config.storage.clear();
1408
+ if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
1190
1409
  this.#emitter.emit("signOut", void 0);
1191
1410
  }
1192
1411
  async #loadSession() {
@@ -1199,8 +1418,23 @@ var AuthManager = class {
1199
1418
  accessToken: stored.accessToken ?? fromConfig.accessToken,
1200
1419
  refreshToken: stored.refreshToken ?? fromConfig.refreshToken
1201
1420
  } : stored ?? fromConfig;
1421
+ this.#seedRefreshCookie();
1202
1422
  return this.#session;
1203
1423
  }
1424
+ /**
1425
+ * Кладёт refresh-токен в jar как cookie `refresh_token`.
1426
+ *
1427
+ * `POST /api/v1/auth/refresh` читает токен только из cookie, поэтому переданный строкой
1428
+ * приходится превращать в неё. В браузере это невозможно — cookie помечена `HttpOnly`,
1429
+ * и там обновление работает только на той, что поставил сам сервер.
1430
+ */
1431
+ #seedRefreshCookie() {
1432
+ if (!this.#config.useCookieJar) return;
1433
+ const refreshToken = this.#session?.refreshToken;
1434
+ if (!refreshToken) return;
1435
+ if (this.#jar.has(REFRESH_COOKIE)) return;
1436
+ this.#jar.set(this.#config.baseUrl, REFRESH_COOKIE, refreshToken, REFRESH_COOKIE_PATH);
1437
+ }
1204
1438
  #sessionFromConfig(auth) {
1205
1439
  if (!auth) return null;
1206
1440
  if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
@@ -1215,7 +1449,12 @@ var AuthManager = class {
1215
1449
  }
1216
1450
  async #saveSession(session) {
1217
1451
  const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
1218
- const next = { ...session, ...cookies?.length ? { cookies } : {} };
1452
+ const deviceId = session.deviceId ?? this.#deviceId;
1453
+ const next = {
1454
+ ...session,
1455
+ ...cookies?.length ? { cookies } : {},
1456
+ ...deviceId ? { deviceId } : {}
1457
+ };
1219
1458
  this.#session = next;
1220
1459
  await this.#config.storage.set(next);
1221
1460
  }
@@ -1234,24 +1473,32 @@ var AuthManager = class {
1234
1473
  }
1235
1474
  async #performRefresh() {
1236
1475
  await this.#loadSession();
1237
- if (!this.hasRefreshSession()) {
1476
+ if (!this.#hasRefreshSession()) {
1238
1477
  return this.#reloginOrNull();
1239
1478
  }
1240
1479
  try {
1241
1480
  const payload = await this.#http.request({
1242
1481
  method: "POST",
1243
1482
  path: AUTH_PATHS.refresh,
1244
- // Обновление опирается на cookie, а не на устаревший Bearer.
1483
+ // Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
1484
+ // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1245
1485
  skipAuth: true,
1246
1486
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1247
1487
  skipAuthRefresh: true,
1248
- ...this.#session?.refreshToken ? { body: { refreshToken: this.#session.refreshToken } } : {}
1488
+ // Обновление почти всегда запускается изнутри запроса, который занимает место
1489
+ // в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
1490
+ skipQueue: true
1249
1491
  });
1250
1492
  const accessToken = readAccessToken(payload);
1251
1493
  if (!accessToken) return this.#reloginOrNull();
1494
+ const rotated = this.#jar.getValue(
1495
+ REFRESH_COOKIE,
1496
+ this.#config.baseUrl + REFRESH_COOKIE_PATH
1497
+ );
1252
1498
  await this.#saveSession({
1253
1499
  ...this.#session ?? {},
1254
1500
  accessToken,
1501
+ ...rotated ? { refreshToken: rotated } : {},
1255
1502
  obtainedAt: Date.now()
1256
1503
  });
1257
1504
  this.#emitter.emit("tokens", { accessToken });
@@ -1259,8 +1506,11 @@ var AuthManager = class {
1259
1506
  } catch (error) {
1260
1507
  if (error instanceof ItdApiError) {
1261
1508
  this.#session = null;
1509
+ this.#jar.clear();
1262
1510
  await this.#config.storage.clear();
1263
- return this.#reloginOrNull();
1511
+ const relogged = await this.#reloginOrNull();
1512
+ if (relogged !== null) return relogged;
1513
+ throw error;
1264
1514
  }
1265
1515
  throw error;
1266
1516
  }
@@ -1272,7 +1522,7 @@ var AuthManager = class {
1272
1522
  return null;
1273
1523
  }
1274
1524
  try {
1275
- return await this.#signInWithCredentials(auth.email, auth.password);
1525
+ return await this.#signInWithCredentials(auth);
1276
1526
  } catch {
1277
1527
  return null;
1278
1528
  }
@@ -1283,21 +1533,41 @@ var AuthManager = class {
1283
1533
  * Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
1284
1534
  * приводить к нескольким попыткам входа и блокировке аккаунта.
1285
1535
  */
1286
- #signInWithCredentials(email, password) {
1536
+ #signInWithCredentials(credentials) {
1287
1537
  if (this.#signingIn) return this.#signingIn;
1288
- const promise = this.#performSignIn(email, password).finally(() => {
1538
+ const promise = this.#performSignIn(credentials).finally(() => {
1289
1539
  this.#signingIn = null;
1290
1540
  });
1291
1541
  this.#signingIn = promise;
1292
1542
  return promise;
1293
1543
  }
1294
- async #performSignIn(email, password) {
1544
+ /**
1545
+ * Берёт токен капчи для входа.
1546
+ *
1547
+ * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
1548
+ * несколько минут, поэтому при повторном входе через сутки годится только свежий.
1549
+ */
1550
+ async #resolveTurnstileToken(credentials) {
1551
+ if (credentials.getTurnstileToken) {
1552
+ const token = await credentials.getTurnstileToken();
1553
+ if (token) return token;
1554
+ }
1555
+ if (credentials.turnstileToken) return credentials.turnstileToken;
1556
+ throw new ItdConfigError(
1557
+ "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY."
1558
+ );
1559
+ }
1560
+ async #performSignIn(credentials) {
1561
+ const turnstileToken = await this.#resolveTurnstileToken(credentials);
1295
1562
  const payload = await this.#http.request({
1296
1563
  method: "POST",
1297
1564
  path: AUTH_PATHS.signIn,
1298
- body: { email, password },
1565
+ body: { email: credentials.email, password: credentials.password, turnstileToken },
1299
1566
  skipAuth: true,
1300
- skipAuthRefresh: true
1567
+ skipAuthRefresh: true,
1568
+ // Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
1569
+ // место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
1570
+ skipQueue: true
1301
1571
  });
1302
1572
  const accessToken = readAccessToken(payload);
1303
1573
  if (!accessToken) {
@@ -1312,43 +1582,6 @@ var AuthManager = class {
1312
1582
  }
1313
1583
  };
1314
1584
 
1315
- // src/core/runtime.ts
1316
- function detectRuntime() {
1317
- const nav = globalThis.navigator;
1318
- if (nav?.product === "ReactNative") return "react-native";
1319
- if (typeof document !== "undefined") return "browser";
1320
- return "server";
1321
- }
1322
- function shouldUseCookieJar(mode) {
1323
- if (mode === "browser") return false;
1324
- if (mode === "server") return true;
1325
- return detectRuntime() === "server";
1326
- }
1327
- function shouldSendCredentials(mode) {
1328
- if (mode === "browser") return true;
1329
- if (mode === "server") return false;
1330
- return detectRuntime() === "browser";
1331
- }
1332
- function resolveFetch(custom) {
1333
- if (custom) return custom;
1334
- if (typeof globalThis.fetch === "function") {
1335
- return globalThis.fetch.bind(globalThis);
1336
- }
1337
- throw new ItdConfigError(
1338
- "\u0412 \u044D\u0442\u043E\u0439 \u0441\u0440\u0435\u0434\u0435 \u043D\u0435\u0442 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E fetch. \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u0435\u0441\u044C \u0434\u043E Node 18+ \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043E\u043F\u0446\u0438\u044E fetch."
1339
- );
1340
- }
1341
- function supportsStreamingBody() {
1342
- return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1343
- }
1344
- function hasLocalStorage() {
1345
- try {
1346
- return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1347
- } catch {
1348
- return false;
1349
- }
1350
- }
1351
-
1352
1585
  // src/core/storage.ts
1353
1586
  var MemoryTokenStorage = class {
1354
1587
  #session = null;
@@ -1463,6 +1696,8 @@ function normalizeBaseUrl(baseUrl) {
1463
1696
  // src/core/config.ts
1464
1697
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1465
1698
  var DEFAULT_TIMEOUT = 3e4;
1699
+ var LIBRARY_VERSION = "0.0.4";
1700
+ var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1466
1701
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1467
1702
  function requirePositive(value, name) {
1468
1703
  if (!Number.isFinite(value) || value < 0) {
@@ -1554,13 +1789,19 @@ function validateAuth(auth) {
1554
1789
  return auth;
1555
1790
  }
1556
1791
  if ("email" in auth || "password" in auth) {
1557
- const { email, password } = auth;
1792
+ const { email, password, turnstileToken, getTurnstileToken } = auth;
1558
1793
  if (typeof email !== "string" || email.trim() === "") {
1559
1794
  throw new ItdConfigError("auth.email \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1560
1795
  }
1561
1796
  if (typeof password !== "string" || password === "") {
1562
1797
  throw new ItdConfigError("auth.password \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1563
1798
  }
1799
+ if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") {
1800
+ throw new ItdConfigError("auth.getTurnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
1801
+ }
1802
+ if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) {
1803
+ throw new ItdConfigError("auth.turnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1804
+ }
1564
1805
  return auth;
1565
1806
  }
1566
1807
  throw new ItdConfigError(
@@ -1568,11 +1809,16 @@ function validateAuth(auth) {
1568
1809
  );
1569
1810
  }
1570
1811
  function resolveConfig(options = {}) {
1571
- const mode = options.mode ?? "auto";
1572
- if (mode !== "auto" && mode !== "browser" && mode !== "server") {
1573
- throw new ItdConfigError(`mode \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C 'auto', 'browser' \u0438\u043B\u0438 'server', \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${mode}`);
1812
+ const mode = options.mode ?? RuntimeMode.Auto;
1813
+ if (!Object.values(RuntimeMode).includes(mode)) {
1814
+ throw new ItdConfigError(
1815
+ `mode \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${Object.values(RuntimeMode).join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${mode}`
1816
+ );
1574
1817
  }
1575
1818
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1819
+ if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
1820
+ throw new ItdConfigError("deviceId \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1821
+ }
1576
1822
  return {
1577
1823
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1578
1824
  auth: validateAuth(options.auth),
@@ -1586,19 +1832,102 @@ function resolveConfig(options = {}) {
1586
1832
  hooks: options.hooks ?? {},
1587
1833
  logger: options.logger === true ? consoleLogger() : options.logger || void 0,
1588
1834
  headers: { ...options.headers },
1835
+ deviceId: options.deviceId,
1836
+ // `false` — способ не слать заголовок вовсе; строка заменяет умолчание.
1837
+ userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
1589
1838
  mode,
1590
1839
  useCookieJar: shouldUseCookieJar(mode),
1591
1840
  sendCredentials: shouldSendCredentials(mode)
1592
1841
  };
1593
1842
  }
1594
1843
 
1595
- // src/core/error-factory.ts
1844
+ // src/core/redact.ts
1845
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1846
+ var SECRET_FIELDS = /* @__PURE__ */ new Set([
1847
+ "password",
1848
+ "oldpassword",
1849
+ "newpassword",
1850
+ "accesstoken",
1851
+ "refreshtoken",
1852
+ "currentpassword",
1853
+ "flowtoken",
1854
+ "token",
1855
+ "turnstiletoken",
1856
+ "otp"
1857
+ ]);
1858
+ function maskSecret(value) {
1859
+ if (value.length <= 8) return "\u2026";
1860
+ return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1861
+ }
1862
+ function redactHeaders(headers) {
1863
+ const result = {};
1864
+ headers.forEach((value, name) => {
1865
+ if (SECRET_HEADERS.has(name.toLowerCase())) {
1866
+ const spaceAt = value.indexOf(" ");
1867
+ result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1868
+ return;
1869
+ }
1870
+ result[name] = value;
1871
+ });
1872
+ return result;
1873
+ }
1874
+ function redactBody(body) {
1875
+ if (body === null || body === void 0) return body;
1876
+ if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1877
+ if (isBlob(body)) return "[Blob]";
1878
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1879
+ if (Array.isArray(body)) return body.map(redactBody);
1880
+ if (typeof body === "object") {
1881
+ const result = {};
1882
+ for (const [key, value] of Object.entries(body)) {
1883
+ result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1884
+ }
1885
+ return result;
1886
+ }
1887
+ return body;
1888
+ }
1889
+
1890
+ // src/core/unwrap.ts
1891
+ function unwrapData(body) {
1892
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1893
+ const keys = Object.keys(body);
1894
+ if (keys.length !== 1 || keys[0] !== "data") return body;
1895
+ return body.data;
1896
+ }
1596
1897
  function isRecord(value) {
1597
1898
  return typeof value === "object" && value !== null && !Array.isArray(value);
1598
1899
  }
1599
1900
  function asString(value) {
1600
1901
  return typeof value === "string" && value.length > 0 ? value : void 0;
1601
1902
  }
1903
+ function pickArray(source, field) {
1904
+ if (typeof source !== "object" || source === null) return [];
1905
+ const value = source[field];
1906
+ return Array.isArray(value) ? value : [];
1907
+ }
1908
+ function pickObject(source, field) {
1909
+ if (typeof source !== "object" || source === null) return void 0;
1910
+ const value = source[field];
1911
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1912
+ return value;
1913
+ }
1914
+ function pickBoolean(source, field, fallback = false) {
1915
+ if (typeof source !== "object" || source === null) return fallback;
1916
+ const value = source[field];
1917
+ return typeof value === "boolean" ? value : fallback;
1918
+ }
1919
+ function pickNumber(source, field, fallback) {
1920
+ if (typeof source !== "object" || source === null) return fallback;
1921
+ const value = source[field];
1922
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1923
+ }
1924
+ function pickString(source, field) {
1925
+ if (typeof source !== "object" || source === null) return void 0;
1926
+ const value = source[field];
1927
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1928
+ }
1929
+
1930
+ // src/core/error-factory.ts
1602
1931
  function collectFieldErrors(source) {
1603
1932
  const result = {};
1604
1933
  const errors = source.errors;
@@ -1707,6 +2036,10 @@ var CODE_TO_CLASS = {
1707
2036
  SESSION_EXPIRED: ItdAuthError,
1708
2037
  SESSION_REVOKED: ItdAuthError,
1709
2038
  SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
2039
+ // Оба приходят с `/auth/refresh`: первый — когда cookie refresh_token не долетела,
2040
+ // второй — когда она есть, но сессия за ней уже мертва.
2041
+ REFRESH_TOKEN_MISSING: ItdAuthError,
2042
+ SESSION_NOT_FOUND: ItdAuthError,
1710
2043
  ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
1711
2044
  ACCESS_DENIED: ItdForbiddenError,
1712
2045
  ENTITY_NOT_FOUND: ItdNotFoundError,
@@ -1724,6 +2057,10 @@ function classByStatus(status) {
1724
2057
  if (status >= 500) return ItdServerError;
1725
2058
  return ItdApiError;
1726
2059
  }
2060
+ function safeRawBody(body) {
2061
+ if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
2062
+ return { ...body, found: redactBody(body.found) };
2063
+ }
1727
2064
  function createApiError(context) {
1728
2065
  const parsed = parseErrorBody(context.body, context.status, context.statusText);
1729
2066
  const rateLimit = readRateLimit(context.headers);
@@ -1739,7 +2076,7 @@ function createApiError(context) {
1739
2076
  requestId: getRequestId(context.headers),
1740
2077
  method: context.method,
1741
2078
  path: context.path,
1742
- raw: context.body,
2079
+ raw: safeRawBody(context.body),
1743
2080
  response: context.response,
1744
2081
  retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
1745
2082
  };
@@ -1750,84 +2087,6 @@ function createApiError(context) {
1750
2087
  return new Ctor(init);
1751
2088
  }
1752
2089
 
1753
- // src/core/redact.ts
1754
- var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1755
- var SECRET_FIELDS = /* @__PURE__ */ new Set([
1756
- "password",
1757
- "oldpassword",
1758
- "newpassword",
1759
- "accesstoken",
1760
- "refreshtoken",
1761
- "flowtoken",
1762
- "token",
1763
- "otp"
1764
- ]);
1765
- function maskSecret(value) {
1766
- if (value.length <= 8) return "\u2026";
1767
- return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1768
- }
1769
- function redactHeaders(headers) {
1770
- const result = {};
1771
- headers.forEach((value, name) => {
1772
- if (SECRET_HEADERS.has(name.toLowerCase())) {
1773
- const spaceAt = value.indexOf(" ");
1774
- result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1775
- return;
1776
- }
1777
- result[name] = value;
1778
- });
1779
- return result;
1780
- }
1781
- function redactBody(body) {
1782
- if (body === null || body === void 0) return body;
1783
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1784
- if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1785
- if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1786
- if (Array.isArray(body)) return body.map(redactBody);
1787
- if (typeof body === "object") {
1788
- const result = {};
1789
- for (const [key, value] of Object.entries(body)) {
1790
- result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1791
- }
1792
- return result;
1793
- }
1794
- return body;
1795
- }
1796
-
1797
- // src/core/unwrap.ts
1798
- function unwrapData(body) {
1799
- if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1800
- const keys = Object.keys(body);
1801
- if (keys.length !== 1 || keys[0] !== "data") return body;
1802
- return body.data;
1803
- }
1804
- function pickArray(source, field) {
1805
- if (typeof source !== "object" || source === null) return [];
1806
- const value = source[field];
1807
- return Array.isArray(value) ? value : [];
1808
- }
1809
- function pickObject(source, field) {
1810
- if (typeof source !== "object" || source === null) return void 0;
1811
- const value = source[field];
1812
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1813
- return value;
1814
- }
1815
- function pickBoolean(source, field, fallback = false) {
1816
- if (typeof source !== "object" || source === null) return fallback;
1817
- const value = source[field];
1818
- return typeof value === "boolean" ? value : fallback;
1819
- }
1820
- function pickNumber(source, field, fallback) {
1821
- if (typeof source !== "object" || source === null) return fallback;
1822
- const value = source[field];
1823
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1824
- }
1825
- function pickString(source, field) {
1826
- if (typeof source !== "object" || source === null) return void 0;
1827
- const value = source[field];
1828
- return typeof value === "string" && value.length > 0 ? value : void 0;
1829
- }
1830
-
1831
2090
  // src/core/http.ts
1832
2091
  function sleep(ms) {
1833
2092
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1843,7 +2102,7 @@ function setHeader(headers, name, value) {
1843
2102
  }
1844
2103
  function isRawBody(body) {
1845
2104
  if (typeof body !== "object" || body === null) return typeof body === "string";
1846
- return typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2105
+ return typeof FormData !== "undefined" && body instanceof FormData || isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
1847
2106
  }
1848
2107
  async function readBody(response) {
1849
2108
  if (response.status === 204 || response.status === 205) return void 0;
@@ -1913,7 +2172,8 @@ var HttpClient = class {
1913
2172
  */
1914
2173
  async request(options) {
1915
2174
  const task = () => this.#withRetries(options);
1916
- return this.#collaborators.schedule ? this.#collaborators.schedule(task) : task();
2175
+ if (!this.#collaborators.schedule || options.skipQueue) return task();
2176
+ return this.#collaborators.schedule(task);
1917
2177
  }
1918
2178
  async #withRetries(options) {
1919
2179
  const method = options.method.toUpperCase();
@@ -1945,6 +2205,11 @@ var HttpClient = class {
1945
2205
  async #buildHeaders(options, url) {
1946
2206
  const headers = new Headers();
1947
2207
  headers.set("Accept", "application/json");
2208
+ headers.set("X-Requested-With", "XMLHttpRequest");
2209
+ if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2210
+ if (this.#collaborators.getDeviceId) {
2211
+ setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
2212
+ }
1948
2213
  for (const [name, value] of Object.entries(this.#config.headers))
1949
2214
  setHeader(headers, name, value);
1950
2215
  if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
@@ -2177,21 +2442,15 @@ function isKnownNotificationType(type) {
2177
2442
  }
2178
2443
 
2179
2444
  // src/notifications/normalize.ts
2180
- function isRecord2(value) {
2181
- return typeof value === "object" && value !== null && !Array.isArray(value);
2182
- }
2183
- function asString2(value) {
2184
- return typeof value === "string" && value.length > 0 ? value : void 0;
2185
- }
2186
2445
  function asActor(value) {
2187
- if (!isRecord2(value)) return void 0;
2188
- const id = asString2(value.id);
2446
+ if (!isRecord(value)) return void 0;
2447
+ const id = asString(value.id);
2189
2448
  if (!id) return void 0;
2190
2449
  return {
2191
2450
  id,
2192
- username: asString2(value.username) ?? "",
2193
- displayName: asString2(value.displayName) ?? "",
2194
- avatar: asString2(value.avatar) ?? "",
2451
+ username: asString(value.username) ?? "",
2452
+ displayName: asString(value.displayName) ?? "",
2453
+ avatar: asString(value.avatar) ?? "",
2195
2454
  ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2196
2455
  ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2197
2456
  };
@@ -2204,33 +2463,34 @@ function readActors(source) {
2204
2463
  return single ? [single] : [];
2205
2464
  }
2206
2465
  function normalizeNotification(input) {
2207
- const source = isRecord2(input) ? input : {};
2208
- const payload = isRecord2(source.payload) ? source.payload : source;
2209
- const rawType = asString2(payload.type) ?? asString2(source.type) ?? "";
2210
- const createdAt = asString2(payload.createdAt) ?? asString2(source.createdAt) ?? "";
2211
- const readAt = asString2(payload.readAt) ?? asString2(source.readAt);
2466
+ const source = isRecord(input) ? input : {};
2467
+ const payload = isRecord(source.payload) ? source.payload : source;
2468
+ const rawType = asString(payload.type) ?? asString(source.type) ?? "";
2469
+ const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
2470
+ const readAt = asString(payload.readAt) ?? asString(source.readAt);
2212
2471
  const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2213
- const subjectId = asString2(payload.subjectId);
2214
- const targetId = asString2(payload.targetId);
2472
+ const subjectId = asString(payload.subjectId);
2473
+ const targetId = asString(payload.targetId);
2215
2474
  const subjectIsComment = payload.subjectType === "comment";
2475
+ const clickUrl = asString(payload.clickUrl);
2216
2476
  return {
2217
- id: asString2(payload.id) ?? asString2(source.id) ?? "",
2477
+ id: asString(payload.id) ?? asString(source.id) ?? "",
2218
2478
  type: canonicalNotificationType(rawType),
2219
2479
  rawType,
2220
- entityId: asString2(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2221
- parentEntityId: asString2(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2480
+ entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2481
+ parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2222
2482
  isRead,
2223
2483
  actors: readActors(payload),
2224
2484
  count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2225
- preview: asString2(payload.entityPreview) ?? asString2(payload.preview) ?? null,
2226
- ...asString2(payload.clickUrl) ? { clickUrl: asString2(payload.clickUrl) } : {},
2485
+ preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
2486
+ ...clickUrl ? { clickUrl } : {},
2227
2487
  createdAt,
2228
- updatedAt: asString2(payload.updatedAt) ?? readAt ?? createdAt,
2488
+ updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
2229
2489
  raw: input
2230
2490
  };
2231
2491
  }
2232
2492
  function readNotificationEvent(data) {
2233
- const source = isRecord2(data) ? data : {};
2493
+ const source = isRecord(data) ? data : {};
2234
2494
  return {
2235
2495
  notification: normalizeNotification(data),
2236
2496
  unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
@@ -2238,8 +2498,8 @@ function readNotificationEvent(data) {
2238
2498
  };
2239
2499
  }
2240
2500
  function readUnreadCountEvent(data) {
2241
- if (!isRecord2(data)) return void 0;
2242
- const payload = isRecord2(data.payload) ? data.payload : void 0;
2501
+ if (!isRecord(data)) return void 0;
2502
+ const payload = isRecord(data.payload) ? data.payload : void 0;
2243
2503
  if (!payload) return void 0;
2244
2504
  return typeof payload.count === "number" ? payload.count : void 0;
2245
2505
  }
@@ -2318,6 +2578,7 @@ var PollTransport = class {
2318
2578
  }
2319
2579
  /** Ждёт следующего опроса, прерываясь при отмене. */
2320
2580
  #wait(signal) {
2581
+ if (signal.aborted) return Promise.resolve();
2321
2582
  return new Promise((resolve) => {
2322
2583
  const timer = setTimeout(finish, this.#interval);
2323
2584
  function finish() {
@@ -2590,6 +2851,14 @@ var SseTransport = class {
2590
2851
  };
2591
2852
 
2592
2853
  // src/realtime/stream.ts
2854
+ var RealtimeTransportKind = Object.freeze({
2855
+ /** Поток событий, если среда умеет читать тело по частям, иначе опрос. */
2856
+ Auto: "auto",
2857
+ /** Поток `text/event-stream`. */
2858
+ Sse: "sse",
2859
+ /** Периодический опрос REST. */
2860
+ Poll: "poll"
2861
+ });
2593
2862
  var ItdRealtime = class {
2594
2863
  #deps;
2595
2864
  #options;
@@ -2597,6 +2866,15 @@ var ItdRealtime = class {
2597
2866
  #transport;
2598
2867
  #maxAttempts;
2599
2868
  #controller;
2869
+ /**
2870
+ * Хочет ли вызывающий код, чтобы соединение было живо.
2871
+ *
2872
+ * Отдельно от `#controller`, потому что тот появляется только после `await` внутри
2873
+ * {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
2874
+ * и подняли два соединения, а `disconnect()` во время ожидания счётчика не был бы
2875
+ * замечен и соединение поднялось бы уже после отмены.
2876
+ */
2877
+ #wanted = false;
2600
2878
  #status = RealtimeStatus.Disconnected;
2601
2879
  #attempt = 0;
2602
2880
  #timer;
@@ -2632,7 +2910,8 @@ var ItdRealtime = class {
2632
2910
  * Возвращает управление сразу после запуска: соединение живёт в фоне.
2633
2911
  */
2634
2912
  async connect() {
2635
- if (this.#controller) return;
2913
+ if (this.#wanted) return;
2914
+ this.#wanted = true;
2636
2915
  this.#attachEnvironmentListeners();
2637
2916
  if (this.#options.syncCount !== false) {
2638
2917
  try {
@@ -2641,10 +2920,11 @@ var ItdRealtime = class {
2641
2920
  this.#deps.logger?.debug("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0447\u0438\u0441\u043B\u043E \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0445", error);
2642
2921
  }
2643
2922
  }
2644
- this.#run();
2923
+ if (this.#wanted) this.#run();
2645
2924
  }
2646
2925
  /** Закрывает соединение и отменяет запланированные попытки. */
2647
2926
  disconnect() {
2927
+ this.#wanted = false;
2648
2928
  if (this.#timer !== void 0) {
2649
2929
  clearTimeout(this.#timer);
2650
2930
  this.#timer = void 0;
@@ -2661,9 +2941,9 @@ var ItdRealtime = class {
2661
2941
  this.#emitter.removeAllListeners();
2662
2942
  }
2663
2943
  #createTransport() {
2664
- const kind = this.#options.transport ?? "auto";
2944
+ const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
2665
2945
  if (typeof kind === "object") return kind;
2666
- if (kind === "poll" || kind === "auto" && !supportsStreamingBody()) {
2946
+ if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) {
2667
2947
  return new PollTransport({
2668
2948
  ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
2669
2949
  });
@@ -2674,6 +2954,7 @@ var ItdRealtime = class {
2674
2954
  }
2675
2955
  /** Запускает попытку подключения; повторы планирует сам. */
2676
2956
  #run() {
2957
+ this.#controller?.abort();
2677
2958
  const controller = new AbortController();
2678
2959
  this.#controller = controller;
2679
2960
  this.#setStatus(RealtimeStatus.Connecting);
@@ -2703,8 +2984,7 @@ var ItdRealtime = class {
2703
2984
  #handleEvent(name, data) {
2704
2985
  this.#emitter.emit("message", { name, data });
2705
2986
  if (name === "connected") {
2706
- const userId = typeof data === "object" && data !== null && "userId" in data ? String(data.userId) : void 0;
2707
- this.#emitter.emit("ready", { userId });
2987
+ this.#emitter.emit("ready", { userId: pickString(data, "userId") });
2708
2988
  return;
2709
2989
  }
2710
2990
  if (name === "notification") {
@@ -2792,13 +3072,22 @@ var ItdRealtime = class {
2792
3072
  };
2793
3073
 
2794
3074
  // src/core/pagination.ts
3075
+ var PaginationMode = Object.freeze({
3076
+ /** Следующая страница запрашивается непрозрачным курсором. */
3077
+ Cursor: "cursor",
3078
+ /** Следующая страница запрашивается номером. */
3079
+ Page: "page",
3080
+ /** Следующая страница запрашивается смещением от начала списка. */
3081
+ Offset: "offset"
3082
+ });
2795
3083
  function readItems(body, fields) {
2796
3084
  if (Array.isArray(body)) return body;
2797
3085
  for (const field of fields) {
2798
3086
  const items = pickArray(body, field);
2799
3087
  if (items.length > 0) return items;
2800
3088
  }
2801
- return fields.length > 0 ? pickArray(body, fields[0]) : [];
3089
+ const primary = fields[0];
3090
+ return primary === void 0 ? [] : pickArray(body, primary);
2802
3091
  }
2803
3092
  function readCursor(body) {
2804
3093
  const pagination = pickObject(body, "pagination");
@@ -2857,12 +3146,13 @@ function readOffsetPage(body, field, offset) {
2857
3146
  var Paginator = class {
2858
3147
  #options;
2859
3148
  #maxPages;
2860
- #state = {};
3149
+ #state;
2861
3150
  #finished = false;
2862
3151
  #pagesLoaded = 0;
2863
3152
  constructor(options) {
2864
3153
  this.#options = options;
2865
3154
  this.#maxPages = options.maxPages ?? 1e3;
3155
+ this.#state = options.start ?? {};
2866
3156
  }
2867
3157
  /**
2868
3158
  * Загружает следующую страницу.
@@ -2928,7 +3218,7 @@ var Paginator = class {
2928
3218
  this.#finished = true;
2929
3219
  return previous;
2930
3220
  }
2931
- if (this.#options.mode === "cursor") {
3221
+ if (this.#options.mode === PaginationMode.Cursor) {
2932
3222
  const cursor = page.nextCursor ?? void 0;
2933
3223
  if (!cursor || cursor === previous.cursor) {
2934
3224
  this.#finished = true;
@@ -2936,7 +3226,7 @@ var Paginator = class {
2936
3226
  }
2937
3227
  return { cursor };
2938
3228
  }
2939
- if (this.#options.mode === "page") {
3229
+ if (this.#options.mode === PaginationMode.Page) {
2940
3230
  return { page: (previous.page ?? 1) + 1 };
2941
3231
  }
2942
3232
  return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
@@ -2965,13 +3255,15 @@ var BaseResource = class {
2965
3255
  *
2966
3256
  * @param mode схема пагинации эндпоинта
2967
3257
  * @param load загружает одну страницу для указанной позиции
3258
+ * @param options `maxPages` и `signal`, а также `start` — позиция, с которой продолжить
2968
3259
  */
2969
3260
  paginate(mode, load, options) {
2970
3261
  return new Paginator({
2971
3262
  mode,
2972
3263
  load,
2973
3264
  ...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
2974
- ...options?.signal !== void 0 ? { signal: options.signal } : {}
3265
+ ...options?.signal !== void 0 ? { signal: options.signal } : {},
3266
+ ...options?.start !== void 0 ? { start: options.start } : {}
2975
3267
  });
2976
3268
  }
2977
3269
  };
@@ -2985,6 +3277,14 @@ function withPageState(query, state) {
2985
3277
  }
2986
3278
 
2987
3279
  // src/resources/auth.ts
3280
+ var OAuthProvider = Object.freeze({
3281
+ Yandex: "yandex",
3282
+ Google: "google"
3283
+ });
3284
+ var SignInStatus = Object.freeze({
3285
+ Authenticated: "authenticated",
3286
+ OtpRequired: "otp_required"
3287
+ });
2988
3288
  var AuthResource = class extends BaseResource {
2989
3289
  #auth;
2990
3290
  constructor(http, deps) {
@@ -3018,6 +3318,8 @@ var AuthResource = class extends BaseResource {
3018
3318
  * тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
3019
3319
  *
3020
3320
  * При успешном входе токен сохраняется в клиенте автоматически.
3321
+ *
3322
+ * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
3021
3323
  */
3022
3324
  async signIn(credentials, options = {}) {
3023
3325
  const body = await this.http.request({
@@ -3031,9 +3333,9 @@ var AuthResource = class extends BaseResource {
3031
3333
  const accessToken = pickString(body, "accessToken");
3032
3334
  if (accessToken) {
3033
3335
  await this.#auth.setAccessToken(accessToken);
3034
- return { status: "authenticated", accessToken };
3336
+ return { status: SignInStatus.Authenticated, accessToken };
3035
3337
  }
3036
- return { status: "otp_required", flowToken: pickString(body, "flowToken") };
3338
+ return { status: SignInStatus.OtpRequired, flowToken: pickString(body, "flowToken") };
3037
3339
  }
3038
3340
  /**
3039
3341
  * Подтверждает вход кодом из письма.
@@ -3088,14 +3390,22 @@ var AuthResource = class extends BaseResource {
3088
3390
  async signInWithOtp(input, options = {}) {
3089
3391
  const { getOtp, ...credentials } = input;
3090
3392
  const result = await this.signIn(credentials, options);
3091
- if (result.status === "authenticated") return result.accessToken;
3393
+ if (result.status === SignInStatus.Authenticated) return result.accessToken;
3092
3394
  if (!result.flowToken) {
3093
3395
  throw new ItdConfigError(
3094
3396
  "\u0421\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u043A\u043E\u0434 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0432\u0445\u043E\u0434 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E"
3095
3397
  );
3096
3398
  }
3097
3399
  const otp = await getOtp();
3098
- return this.verifyOtp({ ...credentials, otp, flowToken: result.flowToken }, options);
3400
+ return this.verifyOtp(
3401
+ {
3402
+ email: credentials.email,
3403
+ password: credentials.password,
3404
+ otp,
3405
+ flowToken: result.flowToken
3406
+ },
3407
+ options
3408
+ );
3099
3409
  }
3100
3410
  /**
3101
3411
  * Обновляет токен доступа.
@@ -3109,9 +3419,18 @@ var AuthResource = class extends BaseResource {
3109
3419
  /**
3110
3420
  * Есть ли признак живой сессии обновления.
3111
3421
  *
3112
- * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном. Позволяет
3113
- * не дёргать API у неавторизованного пользователя. В браузере всегда `true`:
3114
- * cookie ведёт сама среда, и прочитать её из JS нельзя.
3422
+ * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном, а также
3423
+ * refresh-токен, переданный строкой. Позволяет не дёргать API у неавторизованного
3424
+ * пользователя. В браузере всегда `true`: cookie ведёт сама среда, и прочитать её
3425
+ * из JS нельзя.
3426
+ *
3427
+ * Читает {@link TokenStorage}, поэтому результат верен и до первого запроса.
3428
+ *
3429
+ * @example
3430
+ * ```ts
3431
+ * if (await itd.auth.hasRefreshSession()) await itd.auth.refresh();
3432
+ * else redirectToLogin();
3433
+ * ```
3115
3434
  */
3116
3435
  hasRefreshSession() {
3117
3436
  return this.#auth.hasRefreshSession();
@@ -3126,32 +3445,48 @@ var AuthResource = class extends BaseResource {
3126
3445
  });
3127
3446
  await this.#auth.clear();
3128
3447
  }
3129
- /** Завершает все сессии пользователя и очищает локальную. */
3448
+ /**
3449
+ * Завершает все сессии пользователя и очищает локальную.
3450
+ *
3451
+ * Собран из двух запросов, потому что единого эндпоинта на сервере нет:
3452
+ * `POST /api/v1/auth/logout-all` отвечает `404`. Сначала отзываются все прочие сессии
3453
+ * (`DELETE /api/v1/auth/sessions`), затем завершается текущая — в обратном порядке
3454
+ * отзывать было бы уже нечем.
3455
+ */
3130
3456
  async logoutAll(options = {}) {
3131
- await this.http.request({
3132
- method: "POST",
3133
- path: "/api/v1/auth/logout-all",
3134
- skipAuthRefresh: true,
3135
- ...this.requestOptions(options)
3136
- });
3137
- await this.#auth.clear();
3457
+ await this.revokeOtherSessions(options);
3458
+ await this.logout(options);
3138
3459
  }
3139
3460
  /** Забывает сессию локально, не обращаясь к серверу. */
3140
3461
  signOut() {
3141
3462
  return this.#auth.clear();
3142
3463
  }
3143
- /** Запрашивает письмо для сброса пароля. */
3144
- forgotPassword(email, options = {}) {
3145
- return this.http.request({
3464
+ /**
3465
+ * Запрашивает письмо с кодом для сброса пароля.
3466
+ *
3467
+ * @returns `flowToken`, который нужно передать в {@link resetPassword}
3468
+ */
3469
+ async forgotPassword(input, options = {}) {
3470
+ const body = await this.http.request({
3146
3471
  method: "POST",
3147
3472
  path: "/api/v1/auth/forgot-password",
3148
- body: { email },
3473
+ body: input,
3149
3474
  skipAuth: true,
3150
3475
  skipAuthRefresh: true,
3151
3476
  ...this.requestOptions(options)
3152
3477
  });
3478
+ const flowToken = pickString(body, "flowToken");
3479
+ if (!flowToken) {
3480
+ throw new ItdConfigError("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u043F\u0440\u0438 \u0437\u0430\u043F\u0440\u043E\u0441\u0435 \u0441\u0431\u0440\u043E\u0441\u0430 \u043F\u0430\u0440\u043E\u043B\u044F");
3481
+ }
3482
+ return flowToken;
3153
3483
  }
3154
- /** Устанавливает новый пароль по токену из письма. */
3484
+ /**
3485
+ * Устанавливает новый пароль по коду из письма.
3486
+ *
3487
+ * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
3488
+ * при нехватке любого отвечает `422`.
3489
+ */
3155
3490
  resetPassword(input, options = {}) {
3156
3491
  return this.http.request({
3157
3492
  method: "POST",
@@ -3162,12 +3497,45 @@ var AuthResource = class extends BaseResource {
3162
3497
  ...this.requestOptions(options)
3163
3498
  });
3164
3499
  }
3165
- /** Меняет пароль. Требует действующей сессии обновления. */
3500
+ /**
3501
+ * Полный сброс пароля с кодом из письма.
3502
+ *
3503
+ * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
3504
+ * остальное библиотека делает сама.
3505
+ *
3506
+ * @example
3507
+ * ```ts
3508
+ * await itd.auth.resetPasswordWithOtp({
3509
+ * email,
3510
+ * turnstileToken,
3511
+ * newPassword,
3512
+ * getOtp: () => rl.question('Код из письма: '),
3513
+ * });
3514
+ * ```
3515
+ */
3516
+ async resetPasswordWithOtp(input, options = {}) {
3517
+ const flowToken = await this.forgotPassword(
3518
+ { email: input.email, turnstileToken: input.turnstileToken },
3519
+ options
3520
+ );
3521
+ const otp = await input.getOtp();
3522
+ await this.resetPassword(
3523
+ { email: input.email, otp, flowToken, newPassword: input.newPassword },
3524
+ options
3525
+ );
3526
+ }
3527
+ /**
3528
+ * Меняет пароль. Требует действующей сессии.
3529
+ *
3530
+ * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
3531
+ */
3166
3532
  changePassword(input, options = {}) {
3167
3533
  return this.http.request({
3168
3534
  method: "POST",
3169
3535
  path: "/api/v1/auth/change-password",
3170
- body: input,
3536
+ // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3537
+ // не проверить, а лишнее поле он игнорирует.
3538
+ body: { ...input, currentPassword: input.oldPassword },
3171
3539
  ...this.requestOptions(options)
3172
3540
  });
3173
3541
  }
@@ -3237,7 +3605,7 @@ var CommentsResource = class extends BaseResource {
3237
3605
  iterateReplies(commentId, params = {}) {
3238
3606
  const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
3239
3607
  return this.paginate(
3240
- "page",
3608
+ PaginationMode.Page,
3241
3609
  async (state) => {
3242
3610
  const body = await this.http.request({
3243
3611
  method: "GET",
@@ -3247,7 +3615,7 @@ var CommentsResource = class extends BaseResource {
3247
3615
  });
3248
3616
  return readPagedPage(body, "replies");
3249
3617
  },
3250
- params
3618
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
3251
3619
  );
3252
3620
  }
3253
3621
  /**
@@ -3328,7 +3696,11 @@ var IMAGE_MIME_TYPES = Object.freeze([
3328
3696
  "image/heic",
3329
3697
  "image/heif"
3330
3698
  ]);
3331
- var VIDEO_MIME_TYPES = Object.freeze(["video/mp4", "video/webm", "video/quicktime"]);
3699
+ var VIDEO_MIME_TYPES = Object.freeze([
3700
+ "video/mp4",
3701
+ "video/webm",
3702
+ "video/quicktime"
3703
+ ]);
3332
3704
  var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
3333
3705
  var ALLOWED_MIME_TYPES = Object.freeze([
3334
3706
  ...IMAGE_MIME_TYPES,
@@ -3407,7 +3779,7 @@ var FilesResource = class extends BaseResource {
3407
3779
  * ```
3408
3780
  */
3409
3781
  async upload(input, options = {}) {
3410
- const prepared = await this.prepare(input, options);
3782
+ const prepared = await this.#prepare(input, options);
3411
3783
  const form = new FormData();
3412
3784
  form.set("file", prepared.blob, prepared.filename);
3413
3785
  return this.http.request({
@@ -3458,11 +3830,11 @@ var FilesResource = class extends BaseResource {
3458
3830
  });
3459
3831
  }
3460
3832
  /** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
3461
- async prepare(input, options) {
3833
+ async #prepare(input, options) {
3462
3834
  const { data, filename, contentType } = await this.#normalize(input, options);
3463
- const type = contentType ?? ((data instanceof Blob ? data.type : void 0) || mimeFromFilename(filename));
3835
+ const type = contentType ?? ((isBlob(data) ? data.type : void 0) || mimeFromFilename(filename));
3464
3836
  if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
3465
- const blob = data instanceof Blob && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
3837
+ const blob = isBlob(data) && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
3466
3838
  return { blob, filename };
3467
3839
  }
3468
3840
  async #normalize(input, options) {
@@ -3479,8 +3851,8 @@ var FilesResource = class extends BaseResource {
3479
3851
  ...options.contentType ? { contentType: options.contentType } : {}
3480
3852
  };
3481
3853
  }
3482
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input instanceof Blob) {
3483
- const fallbackName = input instanceof File ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
3854
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || isBlob(input)) {
3855
+ const fallbackName = isFile(input) ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
3484
3856
  return {
3485
3857
  data: input,
3486
3858
  filename: options.filename ?? fallbackName,
@@ -3546,7 +3918,7 @@ var HashtagsResource = class extends BaseResource {
3546
3918
  iteratePosts(tag, params = {}) {
3547
3919
  const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
3548
3920
  return this.paginate(
3549
- "cursor",
3921
+ PaginationMode.Cursor,
3550
3922
  async (state) => {
3551
3923
  const body = await this.http.request({
3552
3924
  method: "GET",
@@ -3556,7 +3928,7 @@ var HashtagsResource = class extends BaseResource {
3556
3928
  });
3557
3929
  return readCursorPage(body, "posts");
3558
3930
  },
3559
- params
3931
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3560
3932
  );
3561
3933
  }
3562
3934
  };
@@ -3787,8 +4159,11 @@ var NotificationsResource = class extends BaseResource {
3787
4159
  * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
3788
4160
  * ```
3789
4161
  */
3790
- async list(params = {}) {
3791
- const offset = params.offset ?? 0;
4162
+ list(params = {}) {
4163
+ return this.#loadPage(params, params.offset ?? 0);
4164
+ }
4165
+ /** Общая загрузка страницы для {@link list} и {@link iterate}. */
4166
+ async #loadPage(params, offset) {
3792
4167
  const body = await this.http.request({
3793
4168
  method: "GET",
3794
4169
  // Завершающий слэш обязателен: без него сервер отвечает ошибкой.
@@ -3811,19 +4186,9 @@ var NotificationsResource = class extends BaseResource {
3811
4186
  */
3812
4187
  iterate(params = {}) {
3813
4188
  return this.paginate(
3814
- "offset",
3815
- async (state) => {
3816
- const offset = state.offset ?? params.offset ?? 0;
3817
- const body = await this.http.request({
3818
- method: "GET",
3819
- path: "/api/notifications/",
3820
- query: { limit: params.limit, offset },
3821
- ...this.requestOptions(params)
3822
- });
3823
- const page = readOffsetPage(body, "notifications", offset);
3824
- return { ...page, items: page.items.map(normalizeNotification) };
3825
- },
3826
- params
4189
+ PaginationMode.Offset,
4190
+ (state) => this.#loadPage(params, state.offset ?? 0),
4191
+ { ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
3827
4192
  );
3828
4193
  }
3829
4194
  /** Загружает число непрочитанных уведомлений. */
@@ -3955,7 +4320,7 @@ var PostsResource = class extends BaseResource {
3955
4320
  */
3956
4321
  iterate(params = {}) {
3957
4322
  return this.paginate(
3958
- "cursor",
4323
+ PaginationMode.Cursor,
3959
4324
  async (state) => {
3960
4325
  const body = await this.http.request({
3961
4326
  method: "GET",
@@ -3965,7 +4330,7 @@ var PostsResource = class extends BaseResource {
3965
4330
  });
3966
4331
  return readCursorPage(body, "posts");
3967
4332
  },
3968
- params
4333
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3969
4334
  );
3970
4335
  }
3971
4336
  /**
@@ -4129,7 +4494,7 @@ var PostsResource = class extends BaseResource {
4129
4494
  iterateByUser(user, params = {}) {
4130
4495
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4131
4496
  return this.paginate(
4132
- "cursor",
4497
+ PaginationMode.Cursor,
4133
4498
  async (state) => {
4134
4499
  const body = await this.http.request({
4135
4500
  method: "GET",
@@ -4142,7 +4507,7 @@ var PostsResource = class extends BaseResource {
4142
4507
  });
4143
4508
  return readCursorPage(body, "posts");
4144
4509
  },
4145
- params
4510
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4146
4511
  );
4147
4512
  }
4148
4513
  /** Загружает страницу постов, которые пользователь отметил реакцией. */
@@ -4159,7 +4524,7 @@ var PostsResource = class extends BaseResource {
4159
4524
  iterateLikedByUser(user, params = {}) {
4160
4525
  const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
4161
4526
  return this.paginate(
4162
- "cursor",
4527
+ PaginationMode.Cursor,
4163
4528
  async (state) => {
4164
4529
  const body = await this.http.request({
4165
4530
  method: "GET",
@@ -4169,7 +4534,7 @@ var PostsResource = class extends BaseResource {
4169
4534
  });
4170
4535
  return readCursorPage(body, "posts");
4171
4536
  },
4172
- params
4537
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4173
4538
  );
4174
4539
  }
4175
4540
  /**
@@ -4191,7 +4556,7 @@ var PostsResource = class extends BaseResource {
4191
4556
  iterateComments(postId, params = {}) {
4192
4557
  const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
4193
4558
  return this.paginate(
4194
- "cursor",
4559
+ PaginationMode.Cursor,
4195
4560
  async (state) => {
4196
4561
  const body = await this.http.request({
4197
4562
  method: "GET",
@@ -4201,7 +4566,7 @@ var PostsResource = class extends BaseResource {
4201
4566
  });
4202
4567
  return readFlatCursorPage(body, "comments");
4203
4568
  },
4204
- params
4569
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4205
4570
  );
4206
4571
  }
4207
4572
  /**
@@ -4479,28 +4844,33 @@ var UsersResource = class extends BaseResource {
4479
4844
  ...this.requestOptions(options)
4480
4845
  });
4481
4846
  }
4482
- async #userPage(path, params) {
4847
+ /**
4848
+ * Загружает одну страницу списка пользователей.
4849
+ *
4850
+ * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4851
+ * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4852
+ * список по-своему.
4853
+ */
4854
+ async #loadUserPage(path, params, state) {
4483
4855
  const body = await this.http.request({
4484
4856
  method: "GET",
4485
4857
  path,
4486
- query: { limit: params.limit, page: params.page },
4858
+ query: withPageState({ limit: params.limit }, state),
4487
4859
  ...this.requestOptions(params)
4488
4860
  });
4489
- return readPagedPage(body, "users");
4861
+ return readPagedPage(body, "users", "followers", "following", "blocked");
4862
+ }
4863
+ #userPage(path, params) {
4864
+ return this.#loadUserPage(path, params, {
4865
+ ...params.page !== void 0 ? { page: params.page } : {}
4866
+ });
4490
4867
  }
4491
4868
  #userPaginator(path, params) {
4492
4869
  return this.paginate(
4493
- "page",
4494
- async (state) => {
4495
- const body = await this.http.request({
4496
- method: "GET",
4497
- path,
4498
- query: withPageState({ limit: params.limit }, state),
4499
- ...this.requestOptions(params)
4500
- });
4501
- return readPagedPage(body, "users");
4502
- },
4503
- params
4870
+ PaginationMode.Page,
4871
+ (state) => this.#loadUserPage(path, params, state),
4872
+ // Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
4873
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
4504
4874
  );
4505
4875
  }
4506
4876
  };
@@ -4550,6 +4920,7 @@ var ItdClient = class {
4550
4920
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
4551
4921
  this.#http.setCollaborators({
4552
4922
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4923
+ getDeviceId: () => this.#authManager.getDeviceId(),
4553
4924
  onUnauthorized: () => this.#authManager.onUnauthorized(),
4554
4925
  getCookieHeader: (url) => this.#jar.getHeader(url),
4555
4926
  saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
@@ -4818,6 +5189,6 @@ function toDate(value) {
4818
5189
  return Number.isFinite(date.getTime()) ? date : null;
4819
5190
  }
4820
5191
 
4821
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, RealtimeStatus, ReportReason, ReportTargetType, STREAM_PATH, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdError, isItdRateLimitError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
4822
- //# sourceMappingURL=chunk-76C65H5J.js.map
4823
- //# sourceMappingURL=chunk-76C65H5J.js.map
5192
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
5193
+ //# sourceMappingURL=chunk-JV75JWOX.js.map
5194
+ //# sourceMappingURL=chunk-JV75JWOX.js.map