itd-api 0.0.3 → 0.0.5

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
@@ -622,13 +678,14 @@ var ItdErrorCode = Object.freeze({
622
678
 
623
679
  // src/builders/report.ts
624
680
  var REASONS = new Set(Object.values(ReportReason));
681
+ var TARGET_TYPES = new Set(Object.values(ReportTargetType));
625
682
  function validateReport(input) {
626
683
  if (!input?.targetId || typeof input.targetId !== "string") {
627
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)");
628
685
  }
629
- if (input.targetType !== "post" && input.targetType !== "comment" && input.targetType !== "user") {
686
+ if (!TARGET_TYPES.has(input.targetType)) {
630
687
  throw new ItdConfigError(
631
- `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)}`
632
689
  );
633
690
  }
634
691
  if (!REASONS.has(input.reason)) {
@@ -666,11 +723,11 @@ function start(targetType, targetId) {
666
723
  }
667
724
  var report = Object.freeze({
668
725
  /** Жалоба на пост. */
669
- post: (postId) => start("post", postId),
726
+ post: (postId) => start(ReportTargetType.Post, postId),
670
727
  /** Жалоба на комментарий. */
671
- comment: (commentId) => start("comment", commentId),
728
+ comment: (commentId) => start(ReportTargetType.Comment, commentId),
672
729
  /** Жалоба на пользователя. */
673
- user: (userId) => start("user", userId)
730
+ user: (userId) => start(ReportTargetType.User, userId)
674
731
  });
675
732
  function resolveReport(input) {
676
733
  return resolveInput(input, () => new ReportBuilder({}), validateReport);
@@ -1099,21 +1156,35 @@ var Emitter = class {
1099
1156
  };
1100
1157
 
1101
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
+ });
1102
1173
  function detectRuntime() {
1103
1174
  const nav = globalThis.navigator;
1104
- if (nav?.product === "ReactNative") return "react-native";
1105
- if (typeof document !== "undefined") return "browser";
1106
- return "server";
1175
+ if (nav?.product === "ReactNative") return DetectedRuntime.ReactNative;
1176
+ if (typeof document !== "undefined") return DetectedRuntime.Browser;
1177
+ return DetectedRuntime.Server;
1107
1178
  }
1108
1179
  function shouldUseCookieJar(mode) {
1109
- if (mode === "browser") return false;
1110
- if (mode === "server") return true;
1111
- return detectRuntime() === "server";
1180
+ if (mode === RuntimeMode.Browser) return false;
1181
+ if (mode === RuntimeMode.Server) return true;
1182
+ return detectRuntime() === DetectedRuntime.Server;
1112
1183
  }
1113
1184
  function shouldSendCredentials(mode) {
1114
- if (mode === "browser") return true;
1115
- if (mode === "server") return false;
1116
- return detectRuntime() === "browser";
1185
+ if (mode === RuntimeMode.Browser) return true;
1186
+ if (mode === RuntimeMode.Server) return false;
1187
+ return detectRuntime() === DetectedRuntime.Browser;
1117
1188
  }
1118
1189
  function resolveFetch(custom) {
1119
1190
  if (custom) return custom;
@@ -1124,6 +1195,12 @@ function resolveFetch(custom) {
1124
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."
1125
1196
  );
1126
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
+ }
1127
1204
  function supportsStreamingBody() {
1128
1205
  return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1129
1206
  }
@@ -1198,11 +1275,19 @@ var AuthManager = class {
1198
1275
  /**
1199
1276
  * Есть ли признак живой refresh-сессии.
1200
1277
  *
1201
- * Сайт итд.com ставит рядом с refresh-токеном незакрытую cookie `is_auth` — по ней клиент
1202
- * понимает, что обновление вообще имеет смысл, и не дёргает API у анонимов.
1278
+ * Рядом с refresh-токеном сервер ставит незакрытую cookie `is_auth` — по ней видно,
1279
+ * что продлевать сессию вообще есть смысл, и API не дёргается у анонимов.
1203
1280
  * В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
1281
+ *
1282
+ * Асинхронный, потому что признак может лежать в {@link TokenStorage}: до чтения оттуда
1283
+ * ответ был бы `false` даже при полностью рабочей сохранённой сессии.
1204
1284
  */
1205
- hasRefreshSession() {
1285
+ async hasRefreshSession() {
1286
+ await this.#loadSession();
1287
+ return this.#hasRefreshSession();
1288
+ }
1289
+ /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1290
+ #hasRefreshSession() {
1206
1291
  if (!this.#config.useCookieJar) return true;
1207
1292
  if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1208
1293
  return Boolean(this.#session?.refreshToken);
@@ -1388,7 +1473,7 @@ var AuthManager = class {
1388
1473
  }
1389
1474
  async #performRefresh() {
1390
1475
  await this.#loadSession();
1391
- if (!this.hasRefreshSession()) {
1476
+ if (!this.#hasRefreshSession()) {
1392
1477
  return this.#reloginOrNull();
1393
1478
  }
1394
1479
  try {
@@ -1399,7 +1484,10 @@ var AuthManager = class {
1399
1484
  // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1400
1485
  skipAuth: true,
1401
1486
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1402
- skipAuthRefresh: true
1487
+ skipAuthRefresh: true,
1488
+ // Обновление почти всегда запускается изнутри запроса, который занимает место
1489
+ // в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
1490
+ skipQueue: true
1403
1491
  });
1404
1492
  const accessToken = readAccessToken(payload);
1405
1493
  if (!accessToken) return this.#reloginOrNull();
@@ -1466,7 +1554,7 @@ var AuthManager = class {
1466
1554
  }
1467
1555
  if (credentials.turnstileToken) return credentials.turnstileToken;
1468
1556
  throw new ItdConfigError(
1469
- "\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."
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. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i itd-api-turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
1470
1558
  );
1471
1559
  }
1472
1560
  async #performSignIn(credentials) {
@@ -1476,7 +1564,10 @@ var AuthManager = class {
1476
1564
  path: AUTH_PATHS.signIn,
1477
1565
  body: { email: credentials.email, password: credentials.password, turnstileToken },
1478
1566
  skipAuth: true,
1479
- skipAuthRefresh: true
1567
+ skipAuthRefresh: true,
1568
+ // Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
1569
+ // место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
1570
+ skipQueue: true
1480
1571
  });
1481
1572
  const accessToken = readAccessToken(payload);
1482
1573
  if (!accessToken) {
@@ -1605,7 +1696,7 @@ function normalizeBaseUrl(baseUrl) {
1605
1696
  // src/core/config.ts
1606
1697
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1607
1698
  var DEFAULT_TIMEOUT = 3e4;
1608
- var LIBRARY_VERSION = "0.0.2";
1699
+ var LIBRARY_VERSION = "0.0.5";
1609
1700
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1610
1701
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1611
1702
  function requirePositive(value, name) {
@@ -1718,9 +1809,11 @@ function validateAuth(auth) {
1718
1809
  );
1719
1810
  }
1720
1811
  function resolveConfig(options = {}) {
1721
- const mode = options.mode ?? "auto";
1722
- if (mode !== "auto" && mode !== "browser" && mode !== "server") {
1723
- 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
+ );
1724
1817
  }
1725
1818
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1726
1819
  if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
@@ -1781,7 +1874,7 @@ function redactHeaders(headers) {
1781
1874
  function redactBody(body) {
1782
1875
  if (body === null || body === void 0) return body;
1783
1876
  if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1784
- if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1877
+ if (isBlob(body)) return "[Blob]";
1785
1878
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1786
1879
  if (Array.isArray(body)) return body.map(redactBody);
1787
1880
  if (typeof body === "object") {
@@ -1794,13 +1887,47 @@ function redactBody(body) {
1794
1887
  return body;
1795
1888
  }
1796
1889
 
1797
- // src/core/error-factory.ts
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
+ }
1798
1897
  function isRecord(value) {
1799
1898
  return typeof value === "object" && value !== null && !Array.isArray(value);
1800
1899
  }
1801
1900
  function asString(value) {
1802
1901
  return typeof value === "string" && value.length > 0 ? value : void 0;
1803
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
1804
1931
  function collectFieldErrors(source) {
1805
1932
  const result = {};
1806
1933
  const errors = source.errors;
@@ -1960,40 +2087,6 @@ function createApiError(context) {
1960
2087
  return new Ctor(init);
1961
2088
  }
1962
2089
 
1963
- // src/core/unwrap.ts
1964
- function unwrapData(body) {
1965
- if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1966
- const keys = Object.keys(body);
1967
- if (keys.length !== 1 || keys[0] !== "data") return body;
1968
- return body.data;
1969
- }
1970
- function pickArray(source, field) {
1971
- if (typeof source !== "object" || source === null) return [];
1972
- const value = source[field];
1973
- return Array.isArray(value) ? value : [];
1974
- }
1975
- function pickObject(source, field) {
1976
- if (typeof source !== "object" || source === null) return void 0;
1977
- const value = source[field];
1978
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1979
- return value;
1980
- }
1981
- function pickBoolean(source, field, fallback = false) {
1982
- if (typeof source !== "object" || source === null) return fallback;
1983
- const value = source[field];
1984
- return typeof value === "boolean" ? value : fallback;
1985
- }
1986
- function pickNumber(source, field, fallback) {
1987
- if (typeof source !== "object" || source === null) return fallback;
1988
- const value = source[field];
1989
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1990
- }
1991
- function pickString(source, field) {
1992
- if (typeof source !== "object" || source === null) return void 0;
1993
- const value = source[field];
1994
- return typeof value === "string" && value.length > 0 ? value : void 0;
1995
- }
1996
-
1997
2090
  // src/core/http.ts
1998
2091
  function sleep(ms) {
1999
2092
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2009,7 +2102,7 @@ function setHeader(headers, name, value) {
2009
2102
  }
2010
2103
  function isRawBody(body) {
2011
2104
  if (typeof body !== "object" || body === null) return typeof body === "string";
2012
- 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);
2013
2106
  }
2014
2107
  async function readBody(response) {
2015
2108
  if (response.status === 204 || response.status === 205) return void 0;
@@ -2079,7 +2172,8 @@ var HttpClient = class {
2079
2172
  */
2080
2173
  async request(options) {
2081
2174
  const task = () => this.#withRetries(options);
2082
- return this.#collaborators.schedule ? this.#collaborators.schedule(task) : task();
2175
+ if (!this.#collaborators.schedule || options.skipQueue) return task();
2176
+ return this.#collaborators.schedule(task);
2083
2177
  }
2084
2178
  async #withRetries(options) {
2085
2179
  const method = options.method.toUpperCase();
@@ -2348,21 +2442,15 @@ function isKnownNotificationType(type) {
2348
2442
  }
2349
2443
 
2350
2444
  // src/notifications/normalize.ts
2351
- function isRecord2(value) {
2352
- return typeof value === "object" && value !== null && !Array.isArray(value);
2353
- }
2354
- function asString2(value) {
2355
- return typeof value === "string" && value.length > 0 ? value : void 0;
2356
- }
2357
2445
  function asActor(value) {
2358
- if (!isRecord2(value)) return void 0;
2359
- const id = asString2(value.id);
2446
+ if (!isRecord(value)) return void 0;
2447
+ const id = asString(value.id);
2360
2448
  if (!id) return void 0;
2361
2449
  return {
2362
2450
  id,
2363
- username: asString2(value.username) ?? "",
2364
- displayName: asString2(value.displayName) ?? "",
2365
- avatar: asString2(value.avatar) ?? "",
2451
+ username: asString(value.username) ?? "",
2452
+ displayName: asString(value.displayName) ?? "",
2453
+ avatar: asString(value.avatar) ?? "",
2366
2454
  ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2367
2455
  ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2368
2456
  };
@@ -2375,33 +2463,34 @@ function readActors(source) {
2375
2463
  return single ? [single] : [];
2376
2464
  }
2377
2465
  function normalizeNotification(input) {
2378
- const source = isRecord2(input) ? input : {};
2379
- const payload = isRecord2(source.payload) ? source.payload : source;
2380
- const rawType = asString2(payload.type) ?? asString2(source.type) ?? "";
2381
- const createdAt = asString2(payload.createdAt) ?? asString2(source.createdAt) ?? "";
2382
- 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);
2383
2471
  const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2384
- const subjectId = asString2(payload.subjectId);
2385
- const targetId = asString2(payload.targetId);
2472
+ const subjectId = asString(payload.subjectId);
2473
+ const targetId = asString(payload.targetId);
2386
2474
  const subjectIsComment = payload.subjectType === "comment";
2475
+ const clickUrl = asString(payload.clickUrl);
2387
2476
  return {
2388
- id: asString2(payload.id) ?? asString2(source.id) ?? "",
2477
+ id: asString(payload.id) ?? asString(source.id) ?? "",
2389
2478
  type: canonicalNotificationType(rawType),
2390
2479
  rawType,
2391
- entityId: asString2(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2392
- 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),
2393
2482
  isRead,
2394
2483
  actors: readActors(payload),
2395
2484
  count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2396
- preview: asString2(payload.entityPreview) ?? asString2(payload.preview) ?? null,
2397
- ...asString2(payload.clickUrl) ? { clickUrl: asString2(payload.clickUrl) } : {},
2485
+ preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
2486
+ ...clickUrl ? { clickUrl } : {},
2398
2487
  createdAt,
2399
- updatedAt: asString2(payload.updatedAt) ?? readAt ?? createdAt,
2488
+ updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
2400
2489
  raw: input
2401
2490
  };
2402
2491
  }
2403
2492
  function readNotificationEvent(data) {
2404
- const source = isRecord2(data) ? data : {};
2493
+ const source = isRecord(data) ? data : {};
2405
2494
  return {
2406
2495
  notification: normalizeNotification(data),
2407
2496
  unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
@@ -2409,8 +2498,8 @@ function readNotificationEvent(data) {
2409
2498
  };
2410
2499
  }
2411
2500
  function readUnreadCountEvent(data) {
2412
- if (!isRecord2(data)) return void 0;
2413
- 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;
2414
2503
  if (!payload) return void 0;
2415
2504
  return typeof payload.count === "number" ? payload.count : void 0;
2416
2505
  }
@@ -2489,6 +2578,7 @@ var PollTransport = class {
2489
2578
  }
2490
2579
  /** Ждёт следующего опроса, прерываясь при отмене. */
2491
2580
  #wait(signal) {
2581
+ if (signal.aborted) return Promise.resolve();
2492
2582
  return new Promise((resolve) => {
2493
2583
  const timer = setTimeout(finish, this.#interval);
2494
2584
  function finish() {
@@ -2761,6 +2851,14 @@ var SseTransport = class {
2761
2851
  };
2762
2852
 
2763
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
+ });
2764
2862
  var ItdRealtime = class {
2765
2863
  #deps;
2766
2864
  #options;
@@ -2768,6 +2866,15 @@ var ItdRealtime = class {
2768
2866
  #transport;
2769
2867
  #maxAttempts;
2770
2868
  #controller;
2869
+ /**
2870
+ * Хочет ли вызывающий код, чтобы соединение было живо.
2871
+ *
2872
+ * Отдельно от `#controller`, потому что тот появляется только после `await` внутри
2873
+ * {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
2874
+ * и подняли два соединения, а `disconnect()` во время ожидания счётчика не был бы
2875
+ * замечен и соединение поднялось бы уже после отмены.
2876
+ */
2877
+ #wanted = false;
2771
2878
  #status = RealtimeStatus.Disconnected;
2772
2879
  #attempt = 0;
2773
2880
  #timer;
@@ -2803,7 +2910,8 @@ var ItdRealtime = class {
2803
2910
  * Возвращает управление сразу после запуска: соединение живёт в фоне.
2804
2911
  */
2805
2912
  async connect() {
2806
- if (this.#controller) return;
2913
+ if (this.#wanted) return;
2914
+ this.#wanted = true;
2807
2915
  this.#attachEnvironmentListeners();
2808
2916
  if (this.#options.syncCount !== false) {
2809
2917
  try {
@@ -2812,10 +2920,11 @@ var ItdRealtime = class {
2812
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);
2813
2921
  }
2814
2922
  }
2815
- this.#run();
2923
+ if (this.#wanted) this.#run();
2816
2924
  }
2817
2925
  /** Закрывает соединение и отменяет запланированные попытки. */
2818
2926
  disconnect() {
2927
+ this.#wanted = false;
2819
2928
  if (this.#timer !== void 0) {
2820
2929
  clearTimeout(this.#timer);
2821
2930
  this.#timer = void 0;
@@ -2832,9 +2941,9 @@ var ItdRealtime = class {
2832
2941
  this.#emitter.removeAllListeners();
2833
2942
  }
2834
2943
  #createTransport() {
2835
- const kind = this.#options.transport ?? "auto";
2944
+ const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
2836
2945
  if (typeof kind === "object") return kind;
2837
- if (kind === "poll" || kind === "auto" && !supportsStreamingBody()) {
2946
+ if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) {
2838
2947
  return new PollTransport({
2839
2948
  ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
2840
2949
  });
@@ -2845,6 +2954,7 @@ var ItdRealtime = class {
2845
2954
  }
2846
2955
  /** Запускает попытку подключения; повторы планирует сам. */
2847
2956
  #run() {
2957
+ this.#controller?.abort();
2848
2958
  const controller = new AbortController();
2849
2959
  this.#controller = controller;
2850
2960
  this.#setStatus(RealtimeStatus.Connecting);
@@ -2874,8 +2984,7 @@ var ItdRealtime = class {
2874
2984
  #handleEvent(name, data) {
2875
2985
  this.#emitter.emit("message", { name, data });
2876
2986
  if (name === "connected") {
2877
- const userId = typeof data === "object" && data !== null && "userId" in data ? String(data.userId) : void 0;
2878
- this.#emitter.emit("ready", { userId });
2987
+ this.#emitter.emit("ready", { userId: pickString(data, "userId") });
2879
2988
  return;
2880
2989
  }
2881
2990
  if (name === "notification") {
@@ -2963,13 +3072,22 @@ var ItdRealtime = class {
2963
3072
  };
2964
3073
 
2965
3074
  // src/core/pagination.ts
3075
+ var PaginationMode = Object.freeze({
3076
+ /** Следующая страница запрашивается непрозрачным курсором. */
3077
+ Cursor: "cursor",
3078
+ /** Следующая страница запрашивается номером. */
3079
+ Page: "page",
3080
+ /** Следующая страница запрашивается смещением от начала списка. */
3081
+ Offset: "offset"
3082
+ });
2966
3083
  function readItems(body, fields) {
2967
3084
  if (Array.isArray(body)) return body;
2968
3085
  for (const field of fields) {
2969
3086
  const items = pickArray(body, field);
2970
3087
  if (items.length > 0) return items;
2971
3088
  }
2972
- return fields.length > 0 ? pickArray(body, fields[0]) : [];
3089
+ const primary = fields[0];
3090
+ return primary === void 0 ? [] : pickArray(body, primary);
2973
3091
  }
2974
3092
  function readCursor(body) {
2975
3093
  const pagination = pickObject(body, "pagination");
@@ -3028,12 +3146,13 @@ function readOffsetPage(body, field, offset) {
3028
3146
  var Paginator = class {
3029
3147
  #options;
3030
3148
  #maxPages;
3031
- #state = {};
3149
+ #state;
3032
3150
  #finished = false;
3033
3151
  #pagesLoaded = 0;
3034
3152
  constructor(options) {
3035
3153
  this.#options = options;
3036
3154
  this.#maxPages = options.maxPages ?? 1e3;
3155
+ this.#state = options.start ?? {};
3037
3156
  }
3038
3157
  /**
3039
3158
  * Загружает следующую страницу.
@@ -3099,7 +3218,7 @@ var Paginator = class {
3099
3218
  this.#finished = true;
3100
3219
  return previous;
3101
3220
  }
3102
- if (this.#options.mode === "cursor") {
3221
+ if (this.#options.mode === PaginationMode.Cursor) {
3103
3222
  const cursor = page.nextCursor ?? void 0;
3104
3223
  if (!cursor || cursor === previous.cursor) {
3105
3224
  this.#finished = true;
@@ -3107,7 +3226,7 @@ var Paginator = class {
3107
3226
  }
3108
3227
  return { cursor };
3109
3228
  }
3110
- if (this.#options.mode === "page") {
3229
+ if (this.#options.mode === PaginationMode.Page) {
3111
3230
  return { page: (previous.page ?? 1) + 1 };
3112
3231
  }
3113
3232
  return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
@@ -3136,13 +3255,15 @@ var BaseResource = class {
3136
3255
  *
3137
3256
  * @param mode схема пагинации эндпоинта
3138
3257
  * @param load загружает одну страницу для указанной позиции
3258
+ * @param options `maxPages` и `signal`, а также `start` — позиция, с которой продолжить
3139
3259
  */
3140
3260
  paginate(mode, load, options) {
3141
3261
  return new Paginator({
3142
3262
  mode,
3143
3263
  load,
3144
3264
  ...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
3145
- ...options?.signal !== void 0 ? { signal: options.signal } : {}
3265
+ ...options?.signal !== void 0 ? { signal: options.signal } : {},
3266
+ ...options?.start !== void 0 ? { start: options.start } : {}
3146
3267
  });
3147
3268
  }
3148
3269
  };
@@ -3156,6 +3277,14 @@ function withPageState(query, state) {
3156
3277
  }
3157
3278
 
3158
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
+ });
3159
3288
  var AuthResource = class extends BaseResource {
3160
3289
  #auth;
3161
3290
  constructor(http, deps) {
@@ -3204,9 +3333,9 @@ var AuthResource = class extends BaseResource {
3204
3333
  const accessToken = pickString(body, "accessToken");
3205
3334
  if (accessToken) {
3206
3335
  await this.#auth.setAccessToken(accessToken);
3207
- return { status: "authenticated", accessToken };
3336
+ return { status: SignInStatus.Authenticated, accessToken };
3208
3337
  }
3209
- return { status: "otp_required", flowToken: pickString(body, "flowToken") };
3338
+ return { status: SignInStatus.OtpRequired, flowToken: pickString(body, "flowToken") };
3210
3339
  }
3211
3340
  /**
3212
3341
  * Подтверждает вход кодом из письма.
@@ -3261,7 +3390,7 @@ var AuthResource = class extends BaseResource {
3261
3390
  async signInWithOtp(input, options = {}) {
3262
3391
  const { getOtp, ...credentials } = input;
3263
3392
  const result = await this.signIn(credentials, options);
3264
- if (result.status === "authenticated") return result.accessToken;
3393
+ if (result.status === SignInStatus.Authenticated) return result.accessToken;
3265
3394
  if (!result.flowToken) {
3266
3395
  throw new ItdConfigError(
3267
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"
@@ -3290,9 +3419,18 @@ var AuthResource = class extends BaseResource {
3290
3419
  /**
3291
3420
  * Есть ли признак живой сессии обновления.
3292
3421
  *
3293
- * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном. Позволяет
3294
- * не дёргать API у неавторизованного пользователя. В браузере всегда `true`:
3295
- * 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
+ * ```
3296
3434
  */
3297
3435
  hasRefreshSession() {
3298
3436
  return this.#auth.hasRefreshSession();
@@ -3467,7 +3605,7 @@ var CommentsResource = class extends BaseResource {
3467
3605
  iterateReplies(commentId, params = {}) {
3468
3606
  const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
3469
3607
  return this.paginate(
3470
- "page",
3608
+ PaginationMode.Page,
3471
3609
  async (state) => {
3472
3610
  const body = await this.http.request({
3473
3611
  method: "GET",
@@ -3477,7 +3615,7 @@ var CommentsResource = class extends BaseResource {
3477
3615
  });
3478
3616
  return readPagedPage(body, "replies");
3479
3617
  },
3480
- params
3618
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
3481
3619
  );
3482
3620
  }
3483
3621
  /**
@@ -3558,7 +3696,11 @@ var IMAGE_MIME_TYPES = Object.freeze([
3558
3696
  "image/heic",
3559
3697
  "image/heif"
3560
3698
  ]);
3561
- 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
+ ]);
3562
3704
  var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
3563
3705
  var ALLOWED_MIME_TYPES = Object.freeze([
3564
3706
  ...IMAGE_MIME_TYPES,
@@ -3637,7 +3779,7 @@ var FilesResource = class extends BaseResource {
3637
3779
  * ```
3638
3780
  */
3639
3781
  async upload(input, options = {}) {
3640
- const prepared = await this.prepare(input, options);
3782
+ const prepared = await this.#prepare(input, options);
3641
3783
  const form = new FormData();
3642
3784
  form.set("file", prepared.blob, prepared.filename);
3643
3785
  return this.http.request({
@@ -3688,11 +3830,11 @@ var FilesResource = class extends BaseResource {
3688
3830
  });
3689
3831
  }
3690
3832
  /** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
3691
- async prepare(input, options) {
3833
+ async #prepare(input, options) {
3692
3834
  const { data, filename, contentType } = await this.#normalize(input, options);
3693
- const type = contentType ?? ((data instanceof Blob ? data.type : void 0) || mimeFromFilename(filename));
3835
+ const type = contentType ?? ((isBlob(data) ? data.type : void 0) || mimeFromFilename(filename));
3694
3836
  if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
3695
- 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 ?? "" });
3696
3838
  return { blob, filename };
3697
3839
  }
3698
3840
  async #normalize(input, options) {
@@ -3709,8 +3851,8 @@ var FilesResource = class extends BaseResource {
3709
3851
  ...options.contentType ? { contentType: options.contentType } : {}
3710
3852
  };
3711
3853
  }
3712
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input instanceof Blob) {
3713
- 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);
3714
3856
  return {
3715
3857
  data: input,
3716
3858
  filename: options.filename ?? fallbackName,
@@ -3776,7 +3918,7 @@ var HashtagsResource = class extends BaseResource {
3776
3918
  iteratePosts(tag, params = {}) {
3777
3919
  const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
3778
3920
  return this.paginate(
3779
- "cursor",
3921
+ PaginationMode.Cursor,
3780
3922
  async (state) => {
3781
3923
  const body = await this.http.request({
3782
3924
  method: "GET",
@@ -3786,7 +3928,7 @@ var HashtagsResource = class extends BaseResource {
3786
3928
  });
3787
3929
  return readCursorPage(body, "posts");
3788
3930
  },
3789
- params
3931
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3790
3932
  );
3791
3933
  }
3792
3934
  };
@@ -4017,8 +4159,11 @@ var NotificationsResource = class extends BaseResource {
4017
4159
  * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
4018
4160
  * ```
4019
4161
  */
4020
- async list(params = {}) {
4021
- 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) {
4022
4167
  const body = await this.http.request({
4023
4168
  method: "GET",
4024
4169
  // Завершающий слэш обязателен: без него сервер отвечает ошибкой.
@@ -4041,19 +4186,9 @@ var NotificationsResource = class extends BaseResource {
4041
4186
  */
4042
4187
  iterate(params = {}) {
4043
4188
  return this.paginate(
4044
- "offset",
4045
- async (state) => {
4046
- const offset = state.offset ?? params.offset ?? 0;
4047
- const body = await this.http.request({
4048
- method: "GET",
4049
- path: "/api/notifications/",
4050
- query: { limit: params.limit, offset },
4051
- ...this.requestOptions(params)
4052
- });
4053
- const page = readOffsetPage(body, "notifications", offset);
4054
- return { ...page, items: page.items.map(normalizeNotification) };
4055
- },
4056
- params
4189
+ PaginationMode.Offset,
4190
+ (state) => this.#loadPage(params, state.offset ?? 0),
4191
+ { ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
4057
4192
  );
4058
4193
  }
4059
4194
  /** Загружает число непрочитанных уведомлений. */
@@ -4185,7 +4320,7 @@ var PostsResource = class extends BaseResource {
4185
4320
  */
4186
4321
  iterate(params = {}) {
4187
4322
  return this.paginate(
4188
- "cursor",
4323
+ PaginationMode.Cursor,
4189
4324
  async (state) => {
4190
4325
  const body = await this.http.request({
4191
4326
  method: "GET",
@@ -4195,7 +4330,7 @@ var PostsResource = class extends BaseResource {
4195
4330
  });
4196
4331
  return readCursorPage(body, "posts");
4197
4332
  },
4198
- params
4333
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4199
4334
  );
4200
4335
  }
4201
4336
  /**
@@ -4340,7 +4475,16 @@ var PostsResource = class extends BaseResource {
4340
4475
  });
4341
4476
  return pickArray(body, "posts");
4342
4477
  }
4343
- /** Загружает страницу постов пользователя (его стену). */
4478
+ /**
4479
+ * Загружает страницу стены пользователя.
4480
+ *
4481
+ * Это **не только его собственные посты**: сюда попадают и записи, которые другие
4482
+ * оставили на его стене — у них `author` чужой, а `wallRecipient` указывает на владельца
4483
+ * стены. Поэтому число записей обычно больше, чем `postsCount` из профиля; чтобы
4484
+ * получить только авторские посты, отфильтруйте по `post.author.id`.
4485
+ *
4486
+ * Принимает и UUID, и имя пользователя.
4487
+ */
4344
4488
  async byUser(user, params = {}) {
4345
4489
  const body = await this.http.request({
4346
4490
  method: "GET",
@@ -4355,11 +4499,11 @@ var PostsResource = class extends BaseResource {
4355
4499
  });
4356
4500
  return readCursorPage(body, "posts");
4357
4501
  }
4358
- /** Перебирает посты пользователя. */
4502
+ /** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
4359
4503
  iterateByUser(user, params = {}) {
4360
4504
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4361
4505
  return this.paginate(
4362
- "cursor",
4506
+ PaginationMode.Cursor,
4363
4507
  async (state) => {
4364
4508
  const body = await this.http.request({
4365
4509
  method: "GET",
@@ -4372,7 +4516,7 @@ var PostsResource = class extends BaseResource {
4372
4516
  });
4373
4517
  return readCursorPage(body, "posts");
4374
4518
  },
4375
- params
4519
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4376
4520
  );
4377
4521
  }
4378
4522
  /** Загружает страницу постов, которые пользователь отметил реакцией. */
@@ -4389,7 +4533,7 @@ var PostsResource = class extends BaseResource {
4389
4533
  iterateLikedByUser(user, params = {}) {
4390
4534
  const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
4391
4535
  return this.paginate(
4392
- "cursor",
4536
+ PaginationMode.Cursor,
4393
4537
  async (state) => {
4394
4538
  const body = await this.http.request({
4395
4539
  method: "GET",
@@ -4399,7 +4543,7 @@ var PostsResource = class extends BaseResource {
4399
4543
  });
4400
4544
  return readCursorPage(body, "posts");
4401
4545
  },
4402
- params
4546
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4403
4547
  );
4404
4548
  }
4405
4549
  /**
@@ -4421,7 +4565,7 @@ var PostsResource = class extends BaseResource {
4421
4565
  iterateComments(postId, params = {}) {
4422
4566
  const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
4423
4567
  return this.paginate(
4424
- "cursor",
4568
+ PaginationMode.Cursor,
4425
4569
  async (state) => {
4426
4570
  const body = await this.http.request({
4427
4571
  method: "GET",
@@ -4431,7 +4575,7 @@ var PostsResource = class extends BaseResource {
4431
4575
  });
4432
4576
  return readFlatCursorPage(body, "comments");
4433
4577
  },
4434
- params
4578
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4435
4579
  );
4436
4580
  }
4437
4581
  /**
@@ -4598,19 +4742,34 @@ var UsersResource = class extends BaseResource {
4598
4742
  ...this.requestOptions(options)
4599
4743
  });
4600
4744
  }
4601
- /** Загружает страницу подписчиков. */
4745
+ /**
4746
+ * Загружает подписчиков пользователя.
4747
+ *
4748
+ * ⚠️ **Сервер этот список не листает.** Возвращаются первые 20 записей и только они:
4749
+ * параметр `page` игнорируется (любая страница отдаёт те же записи и `pagination.page: 1`),
4750
+ * `limit` больше 20 молча уменьшается, а `hasMore` всегда `false`. Последнее честно —
4751
+ * получить продолжение нечем.
4752
+ *
4753
+ * Числу `total` доверять тоже не стоит: оно расходится с `followersCount` из профиля —
4754
+ * на проверенных аккаунтах занижено примерно на 1–4%.
4755
+ */
4602
4756
  followers(user, params = {}) {
4603
4757
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4604
4758
  }
4605
- /** Перебирает подписчиков. */
4759
+ /**
4760
+ * Перебирает подписчиков.
4761
+ *
4762
+ * ⚠️ Перебор закончится после первых 20 записей: сервер список не листает —
4763
+ * см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
4764
+ */
4606
4765
  iterateFollowers(user, params = {}) {
4607
4766
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4608
4767
  }
4609
- /** Загружает страницу подписок. */
4768
+ /** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
4610
4769
  following(user, params = {}) {
4611
4770
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4612
4771
  }
4613
- /** Перебирает подписки. */
4772
+ /** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
4614
4773
  iterateFollowing(user, params = {}) {
4615
4774
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4616
4775
  }
@@ -4650,11 +4809,11 @@ var UsersResource = class extends BaseResource {
4650
4809
  ...this.requestOptions(options)
4651
4810
  });
4652
4811
  }
4653
- /** Загружает страницу заблокированных пользователей. */
4812
+ /** Загружает заблокированных пользователей. Ограничения те же, что у {@link followers}. */
4654
4813
  blocked(params = {}) {
4655
4814
  return this.#userPage("/api/users/me/blocked", params);
4656
4815
  }
4657
- /** Перебирает заблокированных пользователей. */
4816
+ /** Перебирает заблокированных. Закончится после первых 20 записей — см. {@link followers}. */
4658
4817
  iterateBlocked(params = {}) {
4659
4818
  return this.#userPaginator("/api/users/me/blocked", params);
4660
4819
  }
@@ -4709,28 +4868,36 @@ var UsersResource = class extends BaseResource {
4709
4868
  ...this.requestOptions(options)
4710
4869
  });
4711
4870
  }
4712
- async #userPage(path, params) {
4871
+ /**
4872
+ * Загружает одну страницу списка пользователей.
4873
+ *
4874
+ * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4875
+ * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4876
+ * список по-своему.
4877
+ *
4878
+ * `page` уходит в запрос, хотя сервер его сейчас не читает (см. {@link followers}):
4879
+ * когда пагинацию починят, работать начнёт само.
4880
+ */
4881
+ async #loadUserPage(path, params, state) {
4713
4882
  const body = await this.http.request({
4714
4883
  method: "GET",
4715
4884
  path,
4716
- query: { limit: params.limit, page: params.page },
4885
+ query: withPageState({ limit: params.limit }, state),
4717
4886
  ...this.requestOptions(params)
4718
4887
  });
4719
- return readPagedPage(body, "users");
4888
+ return readPagedPage(body, "users", "followers", "following", "blocked");
4889
+ }
4890
+ #userPage(path, params) {
4891
+ return this.#loadUserPage(path, params, {
4892
+ ...params.page !== void 0 ? { page: params.page } : {}
4893
+ });
4720
4894
  }
4721
4895
  #userPaginator(path, params) {
4722
4896
  return this.paginate(
4723
- "page",
4724
- async (state) => {
4725
- const body = await this.http.request({
4726
- method: "GET",
4727
- path,
4728
- query: withPageState({ limit: params.limit }, state),
4729
- ...this.requestOptions(params)
4730
- });
4731
- return readPagedPage(body, "users");
4732
- },
4733
- params
4897
+ PaginationMode.Page,
4898
+ (state) => this.#loadUserPage(path, params, state),
4899
+ // Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
4900
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
4734
4901
  );
4735
4902
  }
4736
4903
  };
@@ -5049,6 +5216,6 @@ function toDate(value) {
5049
5216
  return Number.isFinite(date.getTime()) ? date : null;
5050
5217
  }
5051
5218
 
5052
- 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, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, ReportReason, ReportTargetType, STREAM_PATH, TURNSTILE_SITE_KEY, 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 };
5053
- //# sourceMappingURL=chunk-JIT55WDH.js.map
5054
- //# sourceMappingURL=chunk-JIT55WDH.js.map
5219
+ 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 };
5220
+ //# sourceMappingURL=chunk-RUPF4X5L.js.map
5221
+ //# sourceMappingURL=chunk-RUPF4X5L.js.map