itd-api 0.0.1

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.
@@ -0,0 +1,4885 @@
1
+ 'use strict';
2
+
3
+ // src/builders/base.ts
4
+ var BUILDER = /* @__PURE__ */ Symbol.for("itd.builder");
5
+ function isBuilder(value) {
6
+ return typeof value === "object" && value !== null && BUILDER in value;
7
+ }
8
+ function resolveInput(input, factory, validate) {
9
+ if (typeof input === "function") {
10
+ const result = input(factory());
11
+ return isBuilder(result) ? result.build() : validate(result);
12
+ }
13
+ if (isBuilder(input)) return input.build();
14
+ return validate(input);
15
+ }
16
+
17
+ // src/core/errors.ts
18
+ var ITD_ERROR = /* @__PURE__ */ Symbol.for("itd.error");
19
+ var ItdError = class extends Error {
20
+ /** @internal */
21
+ [ITD_ERROR] = true;
22
+ /** Категория ошибки. */
23
+ kind;
24
+ constructor(kind, message, options) {
25
+ super(message, options);
26
+ this.kind = kind;
27
+ this.name = "ItdError";
28
+ }
29
+ };
30
+ var ItdApiError = class extends ItdError {
31
+ /** HTTP-статус ответа. */
32
+ status;
33
+ /** Строковый код ошибки, например `VALIDATION_ERROR`. */
34
+ code;
35
+ /** Расширенное описание, если сервер его прислал. */
36
+ detail;
37
+ /** Заголовок ошибки, если сервер его прислал. */
38
+ title;
39
+ /** Ошибки по полям. Пустой объект, если сервер их не прислал. */
40
+ fieldErrors;
41
+ /** Идентификатор запроса из заголовков ответа. */
42
+ requestId;
43
+ /** HTTP-метод запроса. */
44
+ method;
45
+ /** Путь запроса без базового URL. */
46
+ path;
47
+ /** Тело ответа как оно пришло. */
48
+ raw;
49
+ /** Объект ответа. Тело уже прочитано и повторно прочитано быть не может. */
50
+ response;
51
+ /** Пауза из заголовка `Retry-After` в миллисекундах. Сервер итд.com его не присылает. */
52
+ retryAfter;
53
+ /**
54
+ * Сколько запросов разрешено в окне — заголовок `x-ratelimit-limit`.
55
+ *
56
+ * Времени сброса окна сервер не сообщает, поэтому точный момент повтора неизвестен.
57
+ */
58
+ rateLimit;
59
+ /** Сколько запросов осталось в окне — заголовок `x-ratelimit-remaining`. */
60
+ rateLimitRemaining;
61
+ constructor(init) {
62
+ super("api", init.message);
63
+ this.name = "ItdApiError";
64
+ this.status = init.status;
65
+ this.code = init.code;
66
+ this.detail = init.detail;
67
+ this.title = init.title;
68
+ this.fieldErrors = init.fieldErrors ?? {};
69
+ this.requestId = init.requestId;
70
+ this.method = init.method;
71
+ this.path = init.path;
72
+ this.raw = init.raw;
73
+ this.response = init.response;
74
+ this.retryAfter = init.retryAfter;
75
+ this.rateLimit = init.rateLimit;
76
+ this.rateLimitRemaining = init.rateLimitRemaining;
77
+ }
78
+ /**
79
+ * Проверяет код ошибки. Удобнее, чем сравнивать строки вручную.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * if (err.hasCode('OTP_INVALID', 'MISSING_FLOW_TOKEN')) await restartOtpFlow();
84
+ * ```
85
+ */
86
+ hasCode(...codes) {
87
+ return codes.includes(this.code);
88
+ }
89
+ /** Имеет ли смысл повторить запрос: `429` и серверные ошибки `5xx`. */
90
+ get isRetryable() {
91
+ return this.status === 429 || this.status >= 500;
92
+ }
93
+ };
94
+ var ItdValidationError = class extends ItdApiError {
95
+ constructor(init) {
96
+ super(init);
97
+ this.name = "ItdValidationError";
98
+ }
99
+ };
100
+ var ItdAuthError = class extends ItdApiError {
101
+ constructor(init) {
102
+ super(init);
103
+ this.name = "ItdAuthError";
104
+ }
105
+ };
106
+ var ItdForbiddenError = class extends ItdApiError {
107
+ constructor(init) {
108
+ super(init);
109
+ this.name = "ItdForbiddenError";
110
+ }
111
+ };
112
+ var ItdNotFoundError = class extends ItdApiError {
113
+ constructor(init) {
114
+ super(init);
115
+ this.name = "ItdNotFoundError";
116
+ }
117
+ };
118
+ var ItdConflictError = class extends ItdApiError {
119
+ constructor(init) {
120
+ super(init);
121
+ this.name = "ItdConflictError";
122
+ }
123
+ };
124
+ var ItdRateLimitError = class extends ItdApiError {
125
+ constructor(init) {
126
+ super(init);
127
+ this.name = "ItdRateLimitError";
128
+ }
129
+ };
130
+ var ItdPhoneVerificationError = class extends ItdApiError {
131
+ /** Ссылка на бота подтверждения, если удалось определить идентификатор пользователя. */
132
+ verificationUrl;
133
+ constructor(init) {
134
+ super(init);
135
+ this.name = "ItdPhoneVerificationError";
136
+ this.verificationUrl = init.userId ? `https://t.me/itd_verification_bot?start=${encodeURIComponent(init.userId)}` : void 0;
137
+ }
138
+ };
139
+ var ItdServerError = class extends ItdApiError {
140
+ constructor(init) {
141
+ super(init);
142
+ this.name = "ItdServerError";
143
+ }
144
+ };
145
+ var ItdNetworkError = class extends ItdError {
146
+ /** HTTP-метод запроса. */
147
+ method;
148
+ /** Путь запроса без базового URL. */
149
+ path;
150
+ constructor(message, init) {
151
+ super("network", message, { cause: init.cause });
152
+ this.name = "ItdNetworkError";
153
+ this.method = init.method;
154
+ this.path = init.path;
155
+ }
156
+ };
157
+ var ItdTimeoutError = class extends ItdError {
158
+ /** Значение таймаута в миллисекундах. */
159
+ timeout;
160
+ /** HTTP-метод запроса. */
161
+ method;
162
+ /** Путь запроса без базового URL. */
163
+ path;
164
+ constructor(init) {
165
+ 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`);
166
+ this.name = "ItdTimeoutError";
167
+ this.timeout = init.timeout;
168
+ this.method = init.method;
169
+ this.path = init.path;
170
+ }
171
+ };
172
+ var ItdAbortError = class extends ItdError {
173
+ constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
174
+ super("abort", message);
175
+ this.name = "ItdAbortError";
176
+ }
177
+ };
178
+ var ItdConfigError = class extends ItdError {
179
+ constructor(message) {
180
+ super("config", message);
181
+ this.name = "ItdConfigError";
182
+ }
183
+ };
184
+ function isItdError(value) {
185
+ return typeof value === "object" && value !== null && ITD_ERROR in value;
186
+ }
187
+ function isItdApiError(value) {
188
+ return isItdError(value) && value.kind === "api";
189
+ }
190
+ function isItdValidationError(value) {
191
+ return isItdApiError(value) && value instanceof ItdValidationError;
192
+ }
193
+ function isItdAuthError(value) {
194
+ return isItdApiError(value) && value instanceof ItdAuthError;
195
+ }
196
+ function isItdRateLimitError(value) {
197
+ return isItdApiError(value) && value instanceof ItdRateLimitError;
198
+ }
199
+
200
+ // src/builders/comment.ts
201
+ function validateComment(input) {
202
+ const content = typeof input?.content === "string" ? input.content : "";
203
+ const attachmentIds = input?.attachmentIds ?? [];
204
+ const files = input?.files ?? [];
205
+ const hasContent = content.trim() !== "";
206
+ const hasAttachments = attachmentIds.length > 0 || files.length > 0;
207
+ if (!hasContent && !hasAttachments) {
208
+ throw new ItdConfigError("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043F\u0443\u0441\u0442: \u043D\u0443\u0436\u0435\u043D \u0442\u0435\u043A\u0441\u0442 \u0438\u043B\u0438 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435");
209
+ }
210
+ return input;
211
+ }
212
+ function validateVoice(state) {
213
+ if (state.content.trim() !== "") {
214
+ throw new ItdConfigError(
215
+ "\u0423 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u044F \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0442\u0435\u043A\u0441\u0442\u0430: API \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043B\u0438\u0431\u043E \u0442\u0435\u043A\u0441\u0442, \u043B\u0438\u0431\u043E \u0430\u0443\u0434\u0438\u043E"
216
+ );
217
+ }
218
+ const total = state.files.length + state.attachmentIds.length;
219
+ if (total !== 1) {
220
+ throw new ItdConfigError(
221
+ `\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0439 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u043D\u043E \u0430\u0443\u0434\u0438\u043E\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435, \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E: ${total}`
222
+ );
223
+ }
224
+ }
225
+ var CommentBuilder = class _CommentBuilder {
226
+ /** @internal */
227
+ [BUILDER] = true;
228
+ #state;
229
+ /** @internal Создавайте билдер функцией {@link comment}. */
230
+ constructor(state) {
231
+ this.#state = state;
232
+ }
233
+ /** Задаёт текст комментария. */
234
+ content(text) {
235
+ return new _CommentBuilder({ ...this.#state, content: text });
236
+ }
237
+ /** Прикладывает файл — он будет загружен перед отправкой. */
238
+ attach(file) {
239
+ return new _CommentBuilder({ ...this.#state, files: [...this.#state.files, file] });
240
+ }
241
+ /** Прикладывает уже загруженное вложение. */
242
+ attachId(attachmentId) {
243
+ return new _CommentBuilder({
244
+ ...this.#state,
245
+ attachmentIds: [...this.#state.attachmentIds, attachmentId]
246
+ });
247
+ }
248
+ /**
249
+ * Делает комментарий голосовым.
250
+ *
251
+ * Текста у такого комментария быть не должно, а вложение ровно одно — аудио в формате
252
+ * `audio/ogg`. Так его принимает API.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * await itd.posts.comment(postId, (c) => c.voice('./answer.ogg'));
257
+ * ```
258
+ */
259
+ voice(audio) {
260
+ return new _CommentBuilder({ ...this.#state, files: [audio], voice: true });
261
+ }
262
+ /**
263
+ * Кому адресован ответ.
264
+ *
265
+ * Имеет смысл только в `itd.comments.reply()`; при отправке комментария к посту
266
+ * это поле вызовет ошибку.
267
+ */
268
+ replyTo(userId) {
269
+ return new _CommentBuilder({ ...this.#state, replyToUserId: userId });
270
+ }
271
+ build() {
272
+ const { content, attachmentIds, files, voice, ...rest } = this.#state;
273
+ if (voice) validateVoice(this.#state);
274
+ return validateComment({
275
+ ...rest,
276
+ content,
277
+ ...attachmentIds.length > 0 ? { attachmentIds } : {},
278
+ ...files.length > 0 ? { files } : {}
279
+ });
280
+ }
281
+ toJSON() {
282
+ return this.build();
283
+ }
284
+ };
285
+ function comment(content = "") {
286
+ return new CommentBuilder({ content, attachmentIds: [], files: [], voice: false });
287
+ }
288
+ function resolveComment(input, allowReplyTo = false) {
289
+ const resolved = resolveInput(input, () => comment(), validateComment);
290
+ if (!allowReplyTo && resolved.replyToUserId !== void 0) {
291
+ throw new ItdConfigError(
292
+ "replyTo \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043A \u043E\u0442\u0432\u0435\u0442\u0443 \u043D\u0430 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 (itd.comments.reply). \u0412 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u043A \u043F\u043E\u0441\u0442\u0443 \u0430\u0434\u0440\u0435\u0441\u0430\u0442 \u043D\u0435 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F."
293
+ );
294
+ }
295
+ return resolved;
296
+ }
297
+
298
+ // src/builders/poll.ts
299
+ var MIN_OPTIONS = 2;
300
+ var MAX_OPTIONS = 10;
301
+ var MAX_QUESTION_LENGTH = 200;
302
+ var MAX_OPTION_LENGTH = 100;
303
+ function validatePoll(input) {
304
+ const question = input?.question;
305
+ if (typeof question !== "string" || question.trim() === "") {
306
+ throw new ItdConfigError("\u041E\u043F\u0440\u043E\u0441 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430");
307
+ }
308
+ if (question.trim().length > MAX_QUESTION_LENGTH) {
309
+ throw new ItdConfigError(
310
+ `\u0412\u043E\u043F\u0440\u043E\u0441 \u0434\u043B\u0438\u043D\u043D\u0435\u0435 ${MAX_QUESTION_LENGTH} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 (\u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E ${question.trim().length})`
311
+ );
312
+ }
313
+ const options = Array.isArray(input.options) ? input.options : [];
314
+ const texts = options.map((option, index) => {
315
+ const text = typeof option?.text === "string" ? option.text.trim() : "";
316
+ if (text === "") {
317
+ throw new ItdConfigError(`\u0412\u0430\u0440\u0438\u0430\u043D\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u2116${index + 1} \u043F\u0443\u0441\u0442 \u2014 \u0443 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0442\u0435\u043A\u0441\u0442`);
318
+ }
319
+ if (text.length > MAX_OPTION_LENGTH) {
320
+ throw new ItdConfigError(
321
+ `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u2116${index + 1} \u0434\u043B\u0438\u043D\u043D\u0435\u0435 ${MAX_OPTION_LENGTH} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 (\u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E ${text.length})`
322
+ );
323
+ }
324
+ return text;
325
+ });
326
+ if (texts.length < MIN_OPTIONS) {
327
+ throw new ItdConfigError(
328
+ `\u041E\u043F\u0440\u043E\u0441 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043C\u0438\u043D\u0438\u043C\u0443\u043C ${MIN_OPTIONS} \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E: ${texts.length}`
329
+ );
330
+ }
331
+ if (texts.length > MAX_OPTIONS) {
332
+ throw new ItdConfigError(
333
+ `\u041E\u043F\u0440\u043E\u0441 \u0434\u043E\u043F\u0443\u0441\u043A\u0430\u0435\u0442 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${MAX_OPTIONS} \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432, \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043E: ${texts.length}`
334
+ );
335
+ }
336
+ const seen = /* @__PURE__ */ new Set();
337
+ for (const text of texts) {
338
+ if (seen.has(text)) {
339
+ throw new ItdConfigError(`\u0412\u0430\u0440\u0438\u0430\u043D\u0442 \xAB${text}\xBB \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442\u0441\u044F \u2014 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u0434\u043E\u043B\u0436\u043D\u044B \u0440\u0430\u0437\u043B\u0438\u0447\u0430\u0442\u044C\u0441\u044F`);
340
+ }
341
+ seen.add(text);
342
+ }
343
+ return {
344
+ question: question.trim(),
345
+ options: texts.map((text) => ({ text })),
346
+ // Поле обязательно: без него сервер отвергает создание поста с опросом,
347
+ // даже когда выбор одиночный.
348
+ multipleChoice: input.multipleChoice ?? false
349
+ };
350
+ }
351
+ var PollBuilder = class _PollBuilder {
352
+ /** @internal */
353
+ [BUILDER] = true;
354
+ #state;
355
+ /** @internal Создавайте билдер функцией {@link poll}. */
356
+ constructor(state) {
357
+ this.#state = state;
358
+ }
359
+ /** Задаёт вопрос. */
360
+ question(text) {
361
+ return new _PollBuilder({ ...this.#state, question: text });
362
+ }
363
+ /** Добавляет один вариант ответа. */
364
+ option(text) {
365
+ return new _PollBuilder({ ...this.#state, options: [...this.#state.options, { text }] });
366
+ }
367
+ /**
368
+ * Добавляет несколько вариантов сразу.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * poll('ну как?').options('да', 'нет', 'не знаю');
373
+ * ```
374
+ */
375
+ options(...texts) {
376
+ return new _PollBuilder({
377
+ ...this.#state,
378
+ options: [...this.#state.options, ...texts.map((text) => ({ text }))]
379
+ });
380
+ }
381
+ /** Разрешает выбор нескольких вариантов. */
382
+ multipleChoice(enabled = true) {
383
+ return new _PollBuilder({ ...this.#state, multipleChoice: enabled });
384
+ }
385
+ build() {
386
+ return validatePoll(this.#state);
387
+ }
388
+ toJSON() {
389
+ return this.build();
390
+ }
391
+ };
392
+ function poll(question = "") {
393
+ return new PollBuilder({ question, options: [] });
394
+ }
395
+ function resolvePoll(input) {
396
+ return resolveInput(input, () => poll(), validatePoll);
397
+ }
398
+
399
+ // src/builders/post.ts
400
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
401
+ function validatePost(input) {
402
+ const content = typeof input?.content === "string" ? input.content : "";
403
+ const attachmentIds = input?.attachmentIds ?? [];
404
+ const files = input?.files ?? [];
405
+ const hasContent = content.trim() !== "";
406
+ const hasAttachments = attachmentIds.length > 0 || files.length > 0;
407
+ const hasPoll = Boolean(input?.poll);
408
+ if (!hasContent && !hasAttachments && !hasPoll) {
409
+ throw new ItdConfigError("\u041F\u043E\u0441\u0442 \u043F\u0443\u0441\u0442: \u043D\u0443\u0436\u0435\u043D \u0442\u0435\u043A\u0441\u0442, \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0438\u043B\u0438 \u043E\u043F\u0440\u043E\u0441");
410
+ }
411
+ const wallRecipientId = input.wallRecipientId;
412
+ if (wallRecipientId !== void 0 && wallRecipientId !== null) {
413
+ if (!UUID_PATTERN.test(wallRecipientId)) {
414
+ throw new ItdConfigError(
415
+ `wallRecipientId \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C UUID, \u0430 \u043D\u0435 \u0438\u043C\u0435\u043D\u0435\u043C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F (\u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: \xAB${wallRecipientId}\xBB). \u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043C\u043E\u0436\u043D\u043E \u0432\u0437\u044F\u0442\u044C \u0438\u0437 \u043F\u0440\u043E\u0444\u0438\u043B\u044F: (await itd.users.get(username)).id`
416
+ );
417
+ }
418
+ }
419
+ return input.poll === void 0 ? input : { ...input, poll: resolvePoll(input.poll) };
420
+ }
421
+ var PostBuilder = class _PostBuilder {
422
+ /** @internal */
423
+ [BUILDER] = true;
424
+ #state;
425
+ /** @internal Создавайте билдер функцией {@link post}. */
426
+ constructor(state) {
427
+ this.#state = state;
428
+ }
429
+ /** Задаёт текст поста, заменяя прежний. */
430
+ content(text) {
431
+ return new _PostBuilder({ ...this.#state, content: text });
432
+ }
433
+ /** Дописывает текст к уже заданному. */
434
+ append(text) {
435
+ const separator = this.#state.content === "" ? "" : "\n";
436
+ return new _PostBuilder({ ...this.#state, content: this.#state.content + separator + text });
437
+ }
438
+ /**
439
+ * Задаёт разметку текста.
440
+ *
441
+ * Библиотека разметку не генерирует: хэштеги и упоминания нужно размечать самостоятельно
442
+ * либо не размечать вовсе.
443
+ */
444
+ spans(spans) {
445
+ return new _PostBuilder({ ...this.#state, spans });
446
+ }
447
+ /**
448
+ * Публикует пост на стене другого пользователя.
449
+ *
450
+ * @param userId **UUID** пользователя; имя пользователя не подойдёт
451
+ */
452
+ onWall(userId) {
453
+ return new _PostBuilder({ ...this.#state, wallRecipientId: userId });
454
+ }
455
+ /**
456
+ * Прикладывает файл — он будет загружен перед публикацией.
457
+ *
458
+ * Порядок вызовов сохраняется в порядке вложений.
459
+ */
460
+ attach(file) {
461
+ return new _PostBuilder({ ...this.#state, files: [...this.#state.files, file] });
462
+ }
463
+ /** Прикладывает уже загруженное вложение по его идентификатору. */
464
+ attachId(attachmentId) {
465
+ return new _PostBuilder({
466
+ ...this.#state,
467
+ attachmentIds: [...this.#state.attachmentIds, attachmentId]
468
+ });
469
+ }
470
+ /**
471
+ * Добавляет опрос.
472
+ *
473
+ * Принимает объект, {@link PollBuilder} или функцию-настройщик.
474
+ *
475
+ * @example
476
+ * ```ts
477
+ * post('голосуем').poll((q) => q.question('ну как?').options('да', 'нет'));
478
+ * ```
479
+ */
480
+ poll(input) {
481
+ return new _PostBuilder({ ...this.#state, poll: resolvePoll(input) });
482
+ }
483
+ build() {
484
+ const { content, attachmentIds, files, ...rest } = this.#state;
485
+ return validatePost({
486
+ ...rest,
487
+ ...content !== "" ? { content } : {},
488
+ ...attachmentIds.length > 0 ? { attachmentIds } : {},
489
+ ...files.length > 0 ? { files } : {}
490
+ });
491
+ }
492
+ toJSON() {
493
+ return this.build();
494
+ }
495
+ };
496
+ function post(content = "") {
497
+ return new PostBuilder({ content, attachmentIds: [], files: [] });
498
+ }
499
+ function resolvePost(input) {
500
+ return resolveInput(input, () => post(), validatePost);
501
+ }
502
+
503
+ // src/types/enums.ts
504
+ var FeedTab = Object.freeze({
505
+ /** Популярное. Курсор здесь — номер страницы в виде строки (`"2"`, `"6"`…). */
506
+ Popular: "popular",
507
+ /** Записи тех, на кого вы подписаны. Курсор — отметка времени последнего поста. */
508
+ Following: "following",
509
+ /** Лента клана. Курсор, как и в подписках, — отметка времени. */
510
+ Clan: "clan"
511
+ });
512
+ var CommentSort = Object.freeze({
513
+ /** Сначала новые. */
514
+ Newest: "newest",
515
+ /** Сначала старые. */
516
+ Oldest: "oldest",
517
+ /** Сначала популярные. */
518
+ Popular: "popular"
519
+ });
520
+ var AttachmentType = Object.freeze({
521
+ Image: "image",
522
+ Video: "video",
523
+ /** Голосовые комментарии: `audio/ogg`, с полем `duration`. */
524
+ Audio: "audio"
525
+ });
526
+ var ReportTargetType = Object.freeze({
527
+ Post: "post",
528
+ Comment: "comment",
529
+ User: "user"
530
+ });
531
+ var ReportReason = Object.freeze({
532
+ Spam: "spam",
533
+ Violence: "violence",
534
+ Hate: "hate",
535
+ Adult: "adult",
536
+ Fraud: "fraud",
537
+ Other: "other"
538
+ });
539
+ var RealtimeStatus = Object.freeze({
540
+ Connecting: "connecting",
541
+ Connected: "connected",
542
+ Error: "error",
543
+ Disconnected: "disconnected"
544
+ });
545
+ var WallAccess = Object.freeze({
546
+ Everyone: "everyone"
547
+ });
548
+ var LikesVisibility = Object.freeze({
549
+ Everyone: "everyone",
550
+ /** Только взаимные подписки. */
551
+ Mutual: "mutual"
552
+ });
553
+ var NotificationType = Object.freeze({
554
+ /** Реакция на пост. Старое имя — `like`. */
555
+ PostReaction: "post_reaction",
556
+ /** Комментарий к посту. Старое имя — `comment`. */
557
+ PostComment: "post_comment",
558
+ /** Ответ на комментарий. Старое имя — `reply`. */
559
+ CommentReply: "comment_reply",
560
+ /** Репост. Старое имя — `repost`. */
561
+ PostRepost: "post_repost",
562
+ /** Упоминание в посте. Старое имя — `mention`. */
563
+ PostMention: "post_mention",
564
+ /** Реакция на комментарий. */
565
+ CommentReaction: "comment_reaction",
566
+ /** Упоминание в комментарии. */
567
+ CommentMention: "comment_mention",
568
+ /** Запись на вашей стене. */
569
+ WallPost: "wall_post",
570
+ /** На вас подписались. */
571
+ Follow: "follow",
572
+ /** Заявка на подписку (закрытый профиль). */
573
+ FollowRequest: "follow_request",
574
+ /** Заявка на подписку принята. */
575
+ FollowAccepted: "follow_accepted",
576
+ /** Верификация одобрена. Приходит только по REST. */
577
+ VerificationApproved: "verification_approved",
578
+ /** Верификация отклонена. Приходит только по REST. */
579
+ VerificationRejected: "verification_rejected"
580
+ });
581
+ var ItdErrorCode = Object.freeze({
582
+ BAD_REQUEST: "BAD_REQUEST",
583
+ UNAUTHORIZED: "UNAUTHORIZED",
584
+ ACCESS_DENIED: "ACCESS_DENIED",
585
+ ENTITY_NOT_FOUND: "ENTITY_NOT_FOUND",
586
+ ENTITY_ALREADY_EXISTS: "ENTITY_ALREADY_EXISTS",
587
+ VALIDATION_ERROR: "VALIDATION_ERROR",
588
+ BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
589
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
590
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
591
+ CAPTCHA_FAILED: "CAPTCHA_FAILED",
592
+ OTP_INVALID: "OTP_INVALID",
593
+ ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
594
+ ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
595
+ ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
596
+ ACCOUNT_TEMPORARILY_LOCKED: "ACCOUNT_TEMPORARILY_LOCKED",
597
+ ACCOUNT_CURRENT_PASSWORD_INCORRECT: "ACCOUNT_CURRENT_PASSWORD_INCORRECT",
598
+ SESSION_EXPIRED: "SESSION_EXPIRED",
599
+ SESSION_REVOKED: "SESSION_REVOKED",
600
+ SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
601
+ MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
602
+ PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
603
+ PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
604
+ PROFILE_MODIFICATION_RESTRICTED: "PROFILE_MODIFICATION_RESTRICTED",
605
+ CONTENT_MODERATION_FAILED: "CONTENT_MODERATION_FAILED",
606
+ FILE_TOO_LARGE: "FILE_TOO_LARGE",
607
+ UNSUPPORTED_FILE_TYPE: "UNSUPPORTED_FILE_TYPE",
608
+ UPLOAD_FAILED: "UPLOAD_FAILED",
609
+ VIDEO_REQUIRES_VERIFICATION: "VIDEO_REQUIRES_VERIFICATION",
610
+ PHONE_VERIFICATION_REQUIRED: "PHONE_VERIFICATION_REQUIRED",
611
+ WRITE_ACCESS_RESTRICTED: "WRITE_ACCESS_RESTRICTED"
612
+ });
613
+
614
+ // src/builders/report.ts
615
+ var REASONS = new Set(Object.values(ReportReason));
616
+ function validateReport(input) {
617
+ if (!input?.targetId || typeof input.targetId !== "string") {
618
+ 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)");
619
+ }
620
+ if (input.targetType !== "post" && input.targetType !== "comment" && input.targetType !== "user") {
621
+ throw new ItdConfigError(
622
+ `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)}`
623
+ );
624
+ }
625
+ if (!REASONS.has(input.reason)) {
626
+ throw new ItdConfigError(
627
+ `\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0436\u0430\u043B\u043E\u0431\u044B \xAB${String(input.reason)}\xBB. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: ${[...REASONS].join(", ")}`
628
+ );
629
+ }
630
+ return input;
631
+ }
632
+ var ReportBuilder = class _ReportBuilder {
633
+ /** @internal */
634
+ [BUILDER] = true;
635
+ #state;
636
+ /** @internal Создавайте билдер через {@link report}. */
637
+ constructor(state) {
638
+ this.#state = state;
639
+ }
640
+ /** Указывает причину жалобы. */
641
+ reason(reason) {
642
+ return new _ReportBuilder({ ...this.#state, reason });
643
+ }
644
+ /** Добавляет пояснение в свободной форме. */
645
+ description(text) {
646
+ return new _ReportBuilder({ ...this.#state, description: text });
647
+ }
648
+ build() {
649
+ return validateReport(this.#state);
650
+ }
651
+ toJSON() {
652
+ return this.build();
653
+ }
654
+ };
655
+ function start(targetType, targetId) {
656
+ return new ReportBuilder({ targetType, targetId });
657
+ }
658
+ var report = Object.freeze({
659
+ /** Жалоба на пост. */
660
+ post: (postId) => start("post", postId),
661
+ /** Жалоба на комментарий. */
662
+ comment: (commentId) => start("comment", commentId),
663
+ /** Жалоба на пользователя. */
664
+ user: (userId) => start("user", userId)
665
+ });
666
+ function resolveReport(input) {
667
+ return resolveInput(input, () => new ReportBuilder({}), validateReport);
668
+ }
669
+
670
+ // node_modules/set-cookie-parser/lib/set-cookie.js
671
+ var defaultParseOptions = {
672
+ decodeValues: true,
673
+ map: false,
674
+ silent: false,
675
+ split: "auto"
676
+ // auto = split strings but not arrays
677
+ };
678
+ function isForbiddenKey(key) {
679
+ return typeof key !== "string" || key in {};
680
+ }
681
+ function createNullObj() {
682
+ return /* @__PURE__ */ Object.create(null);
683
+ }
684
+ function isNonEmptyString(str) {
685
+ return typeof str === "string" && !!str.trim();
686
+ }
687
+ function parseString(setCookieValue, options) {
688
+ var parts = setCookieValue.split(";").filter(isNonEmptyString);
689
+ var nameValuePairStr = parts.shift();
690
+ if (!nameValuePairStr) {
691
+ return null;
692
+ }
693
+ var parsed = parseNameValuePair(nameValuePairStr);
694
+ var name = parsed.name;
695
+ var value = parsed.value;
696
+ options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions;
697
+ if (isForbiddenKey(name)) {
698
+ return null;
699
+ }
700
+ try {
701
+ value = options.decodeValues ? decodeURIComponent(value) : value;
702
+ } catch (e) {
703
+ console.error(
704
+ "set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.",
705
+ e
706
+ );
707
+ }
708
+ var cookie = createNullObj();
709
+ cookie.name = name;
710
+ cookie.value = value;
711
+ parts.forEach(function(part) {
712
+ var sides = part.split("=");
713
+ var key = sides.shift().trim().toLowerCase();
714
+ if (isForbiddenKey(key)) {
715
+ return;
716
+ }
717
+ var value2 = sides.join("=").trim();
718
+ if (key === "expires") {
719
+ cookie.expires = new Date(value2);
720
+ } else if (key === "max-age") {
721
+ var n = parseInt(value2, 10);
722
+ if (!Number.isNaN(n)) cookie.maxAge = n;
723
+ } else if (key === "secure") {
724
+ cookie.secure = true;
725
+ } else if (key === "httponly") {
726
+ cookie.httpOnly = true;
727
+ } else if (key === "samesite") {
728
+ cookie.sameSite = value2;
729
+ } else if (key === "partitioned") {
730
+ cookie.partitioned = true;
731
+ } else if (key) {
732
+ cookie[key] = value2;
733
+ }
734
+ });
735
+ return cookie;
736
+ }
737
+ function parseNameValuePair(nameValuePairStr) {
738
+ var name = "";
739
+ var value = "";
740
+ var nameValueArr = nameValuePairStr.split("=");
741
+ if (nameValueArr.length > 1) {
742
+ name = nameValueArr.shift();
743
+ value = nameValueArr.join("=");
744
+ } else {
745
+ value = nameValuePairStr;
746
+ }
747
+ return { name, value };
748
+ }
749
+ function parseSetCookie(input, options) {
750
+ options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions;
751
+ if (!input) {
752
+ if (!options.map) {
753
+ return [];
754
+ } else {
755
+ return createNullObj();
756
+ }
757
+ }
758
+ if (input.headers) {
759
+ if (typeof input.headers.getSetCookie === "function") {
760
+ input = input.headers.getSetCookie();
761
+ } else if (input.headers["set-cookie"]) {
762
+ input = input.headers["set-cookie"];
763
+ } else {
764
+ var sch = input.headers[Object.keys(input.headers).find(function(key) {
765
+ return key.toLowerCase() === "set-cookie";
766
+ })];
767
+ if (!sch && input.headers.cookie && !options.silent) {
768
+ console.warn(
769
+ "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning."
770
+ );
771
+ }
772
+ input = sch;
773
+ }
774
+ }
775
+ var split = options.split;
776
+ var isArray = Array.isArray(input);
777
+ if (split === "auto") {
778
+ split = !isArray;
779
+ }
780
+ if (!isArray) {
781
+ input = [input];
782
+ }
783
+ input = input.filter(isNonEmptyString);
784
+ if (split) {
785
+ input = input.map(splitCookiesString).flat();
786
+ }
787
+ if (!options.map) {
788
+ return input.map(function(str) {
789
+ return parseString(str, options);
790
+ }).filter(Boolean);
791
+ } else {
792
+ var cookies = createNullObj();
793
+ return input.reduce(function(cookies2, str) {
794
+ var cookie = parseString(str, options);
795
+ if (cookie && !isForbiddenKey(cookie.name)) {
796
+ cookies2[cookie.name] = cookie;
797
+ }
798
+ return cookies2;
799
+ }, cookies);
800
+ }
801
+ }
802
+ function splitCookiesString(cookiesString) {
803
+ if (Array.isArray(cookiesString)) {
804
+ return cookiesString;
805
+ }
806
+ if (typeof cookiesString !== "string") {
807
+ return [];
808
+ }
809
+ var cookiesStrings = [];
810
+ var pos = 0;
811
+ var start2;
812
+ var ch;
813
+ var lastComma;
814
+ var nextStart;
815
+ var cookiesSeparatorFound;
816
+ function skipWhitespace() {
817
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
818
+ pos += 1;
819
+ }
820
+ return pos < cookiesString.length;
821
+ }
822
+ function notSpecialChar() {
823
+ ch = cookiesString.charAt(pos);
824
+ return ch !== "=" && ch !== ";" && ch !== ",";
825
+ }
826
+ while (pos < cookiesString.length) {
827
+ start2 = pos;
828
+ cookiesSeparatorFound = false;
829
+ while (skipWhitespace()) {
830
+ ch = cookiesString.charAt(pos);
831
+ if (ch === ",") {
832
+ lastComma = pos;
833
+ pos += 1;
834
+ skipWhitespace();
835
+ nextStart = pos;
836
+ while (pos < cookiesString.length && notSpecialChar()) {
837
+ pos += 1;
838
+ }
839
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
840
+ cookiesSeparatorFound = true;
841
+ pos = nextStart;
842
+ cookiesStrings.push(cookiesString.substring(start2, lastComma));
843
+ start2 = pos;
844
+ } else {
845
+ pos = lastComma + 1;
846
+ }
847
+ } else {
848
+ pos += 1;
849
+ }
850
+ }
851
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
852
+ cookiesStrings.push(cookiesString.substring(start2, cookiesString.length));
853
+ }
854
+ }
855
+ return cookiesStrings;
856
+ }
857
+ parseSetCookie.parseSetCookie = parseSetCookie;
858
+ parseSetCookie.parse = parseSetCookie;
859
+ parseSetCookie.parseString = parseString;
860
+ parseSetCookie.splitCookiesString = splitCookiesString;
861
+
862
+ // src/core/cookies.ts
863
+ var AUTH_FLAG_COOKIE = "is_auth";
864
+ var SERIALIZED_SEPARATOR = " ";
865
+ function originOf(url) {
866
+ try {
867
+ return new URL(url).origin;
868
+ } catch {
869
+ return "";
870
+ }
871
+ }
872
+ function toTimestamp(date) {
873
+ if (!date) return void 0;
874
+ const time = date.getTime();
875
+ return Number.isFinite(time) ? time : void 0;
876
+ }
877
+ function pathOf(url) {
878
+ try {
879
+ return new URL(url).pathname || "/";
880
+ } catch {
881
+ return "/";
882
+ }
883
+ }
884
+ function pathMatches(cookiePath, requestPath) {
885
+ if (cookiePath === "/" || cookiePath === requestPath) return true;
886
+ if (!requestPath.startsWith(cookiePath)) return false;
887
+ return cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/";
888
+ }
889
+ var CookieJar = class {
890
+ #byOrigin = /* @__PURE__ */ new Map();
891
+ /**
892
+ * Забирает `Set-Cookie` из ответа.
893
+ *
894
+ * Использует `Headers.getSetCookie()`, где он есть (Node 20+, undici). В остальных средах
895
+ * заголовки склеены в одну строку, и её нельзя резать по запятой напрямую: запятая есть
896
+ * внутри `Expires=Wed, 09 Jun 2027 …`. Разделением занимается `set-cookie-parser`.
897
+ */
898
+ setFromResponse(url, response) {
899
+ const headers = response.headers;
900
+ const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
901
+ if (raw.length > 0) this.setFromStrings(url, raw);
902
+ }
903
+ /** Сохраняет cookie из готовых строк `Set-Cookie`. */
904
+ setFromStrings(url, setCookieStrings) {
905
+ const origin = originOf(url);
906
+ if (!origin) return;
907
+ const jar = this.#byOrigin.get(origin) ?? /* @__PURE__ */ new Map();
908
+ for (const parsed of parseSetCookie(setCookieStrings)) {
909
+ const expires = toTimestamp(parsed.expires);
910
+ const maxAgeExpires = typeof parsed.maxAge === "number" && Number.isFinite(parsed.maxAge) ? Date.now() + parsed.maxAge * 1e3 : void 0;
911
+ const expiresAt = maxAgeExpires ?? expires;
912
+ if (expiresAt !== void 0 && expiresAt <= Date.now()) {
913
+ jar.delete(parsed.name);
914
+ continue;
915
+ }
916
+ jar.set(parsed.name, {
917
+ name: parsed.name,
918
+ value: parsed.value,
919
+ path: parsed.path ?? "/",
920
+ expires: expiresAt,
921
+ secure: parsed.secure ?? false
922
+ });
923
+ }
924
+ this.#byOrigin.set(origin, jar);
925
+ }
926
+ /**
927
+ * Собирает значение заголовка `Cookie` для запроса.
928
+ *
929
+ * @returns строка вида `a=1; b=2` либо `undefined`, если подходящих cookie нет
930
+ */
931
+ getHeader(url) {
932
+ const cookies = this.#matching(url);
933
+ if (cookies.length === 0) return void 0;
934
+ return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
935
+ }
936
+ /**
937
+ * Есть ли действующая cookie с таким именем.
938
+ *
939
+ * Используется для проверки флага {@link AUTH_FLAG_COOKIE} перед запросом обновления
940
+ * токена: у анонима refresh-сессии нет, и дёргать API незачем.
941
+ */
942
+ has(name, url) {
943
+ if (url) return this.#matching(url).some((cookie) => cookie.name === name);
944
+ const now = Date.now();
945
+ for (const jar of this.#byOrigin.values()) {
946
+ const cookie = jar.get(name);
947
+ if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return true;
948
+ }
949
+ return false;
950
+ }
951
+ /** Сохраняет содержимое jar для записи в {@link TokenStorage}. */
952
+ serialize() {
953
+ const result = [];
954
+ const now = Date.now();
955
+ for (const [origin, jar] of this.#byOrigin) {
956
+ for (const cookie of jar.values()) {
957
+ if (cookie.expires !== void 0 && cookie.expires <= now) continue;
958
+ const parts = [`${cookie.name}=${cookie.value}`, `Path=${cookie.path}`];
959
+ if (cookie.expires !== void 0) {
960
+ parts.push(`Expires=${new Date(cookie.expires).toUTCString()}`);
961
+ }
962
+ if (cookie.secure) parts.push("Secure");
963
+ result.push(`${origin}${SERIALIZED_SEPARATOR}${parts.join("; ")}`);
964
+ }
965
+ }
966
+ return result;
967
+ }
968
+ /** Восстанавливает jar из результата {@link serialize}. Некорректные записи молча пропускаются. */
969
+ deserialize(entries) {
970
+ if (!entries) return;
971
+ for (const entry of entries) {
972
+ const separatorAt = entry.indexOf(SERIALIZED_SEPARATOR);
973
+ if (separatorAt <= 0) continue;
974
+ const origin = entry.slice(0, separatorAt);
975
+ const setCookie = entry.slice(separatorAt + 1);
976
+ if (!originOf(origin)) continue;
977
+ this.setFromStrings(origin, [setCookie]);
978
+ }
979
+ }
980
+ /** Удаляет все cookie. */
981
+ clear() {
982
+ this.#byOrigin.clear();
983
+ }
984
+ /** Действующие cookie, подходящие запросу: тот же origin, подходящий путь, не истёкшие. */
985
+ #matching(url) {
986
+ const origin = originOf(url);
987
+ const jar = this.#byOrigin.get(origin);
988
+ if (!jar) return [];
989
+ const isSecureRequest = origin.startsWith("https:");
990
+ const requestPath = pathOf(url);
991
+ const now = Date.now();
992
+ const result = [];
993
+ for (const cookie of jar.values()) {
994
+ if (cookie.expires !== void 0 && cookie.expires <= now) {
995
+ jar.delete(cookie.name);
996
+ continue;
997
+ }
998
+ if (cookie.secure && !isSecureRequest) continue;
999
+ if (!pathMatches(cookie.path, requestPath)) continue;
1000
+ result.push(cookie);
1001
+ }
1002
+ return result;
1003
+ }
1004
+ };
1005
+
1006
+ // src/core/emitter.ts
1007
+ var Emitter = class {
1008
+ #listeners = /* @__PURE__ */ new Map();
1009
+ #onError;
1010
+ constructor(onListenerError) {
1011
+ this.#onError = onListenerError;
1012
+ }
1013
+ /**
1014
+ * Подписывается на событие.
1015
+ *
1016
+ * @returns функция отписки
1017
+ *
1018
+ * @example
1019
+ * ```ts
1020
+ * const off = realtime.on('notification', (event) => console.log(event));
1021
+ * off();
1022
+ * ```
1023
+ */
1024
+ on(event, listener) {
1025
+ const set = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
1026
+ set.add(listener);
1027
+ this.#listeners.set(event, set);
1028
+ return () => this.off(event, listener);
1029
+ }
1030
+ /** Подписывается на одно срабатывание. */
1031
+ once(event, listener) {
1032
+ const off = this.on(event, (payload) => {
1033
+ off();
1034
+ listener(payload);
1035
+ });
1036
+ return off;
1037
+ }
1038
+ /** Отписывается от события. */
1039
+ off(event, listener) {
1040
+ this.#listeners.get(event)?.delete(listener);
1041
+ }
1042
+ /** Рассылает событие подписчикам. */
1043
+ emit(event, payload) {
1044
+ const set = this.#listeners.get(event);
1045
+ if (!set) return;
1046
+ for (const listener of [...set]) {
1047
+ try {
1048
+ listener(payload);
1049
+ } catch (error) {
1050
+ this.#onError?.(error);
1051
+ }
1052
+ }
1053
+ }
1054
+ /** Сколько подписчиков у события. */
1055
+ listenerCount(event) {
1056
+ return this.#listeners.get(event)?.size ?? 0;
1057
+ }
1058
+ /** Снимает все подписки. */
1059
+ removeAllListeners() {
1060
+ this.#listeners.clear();
1061
+ }
1062
+ };
1063
+
1064
+ // src/core/auth.ts
1065
+ var AUTH_PATHS = {
1066
+ signIn: "/api/v1/auth/sign-in",
1067
+ refresh: "/api/v1/auth/refresh"
1068
+ };
1069
+ function readAccessToken(payload) {
1070
+ if (typeof payload !== "object" || payload === null) return void 0;
1071
+ const token = payload.accessToken;
1072
+ return typeof token === "string" && token.length > 0 ? token : void 0;
1073
+ }
1074
+ var AuthManager = class {
1075
+ #config;
1076
+ #http;
1077
+ #jar;
1078
+ #emitter = new Emitter();
1079
+ /** `undefined` — сессия ещё не читалась из хранилища. */
1080
+ #session;
1081
+ /** Общий промис обновления: к нему присоединяются все, кто получил 401. */
1082
+ #refreshing = null;
1083
+ /** Общий промис входа по логину и паролю. */
1084
+ #signingIn = null;
1085
+ constructor(config, http, jar) {
1086
+ this.#config = config;
1087
+ this.#http = http;
1088
+ this.#jar = jar;
1089
+ }
1090
+ /** Подписка на события авторизации. */
1091
+ get on() {
1092
+ return this.#emitter.on.bind(this.#emitter);
1093
+ }
1094
+ /** Подписка на одно срабатывание. */
1095
+ get once() {
1096
+ return this.#emitter.once.bind(this.#emitter);
1097
+ }
1098
+ /**
1099
+ * Есть ли признак живой refresh-сессии.
1100
+ *
1101
+ * Сайт итд.com ставит рядом с refresh-токеном незакрытую cookie `is_auth` — по ней клиент
1102
+ * понимает, что обновление вообще имеет смысл, и не дёргает API у анонимов.
1103
+ * В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
1104
+ */
1105
+ hasRefreshSession() {
1106
+ if (!this.#config.useCookieJar) return true;
1107
+ if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1108
+ return Boolean(this.#session?.refreshToken);
1109
+ }
1110
+ /** Заголовки авторизации для очередного запроса. Пустой объект, если токена нет. */
1111
+ async getAuthHeaders() {
1112
+ const token = await this.getAccessToken();
1113
+ return token ? { Authorization: `Bearer ${token}` } : {};
1114
+ }
1115
+ /**
1116
+ * Текущий токен доступа.
1117
+ *
1118
+ * При необходимости выполняет отложенный вход: если в конфигурации переданы логин
1119
+ * и пароль, первый же запрос сам заведёт сессию.
1120
+ */
1121
+ async getAccessToken() {
1122
+ const session = await this.#loadSession();
1123
+ if (session?.accessToken) return session.accessToken;
1124
+ const auth = this.#config.auth;
1125
+ if (!auth) return null;
1126
+ if (typeof auth === "object" && "getToken" in auth) {
1127
+ return await auth.getToken() ?? null;
1128
+ }
1129
+ if (typeof auth === "object" && "email" in auth) {
1130
+ return this.#signInWithCredentials(auth.email, auth.password);
1131
+ }
1132
+ return null;
1133
+ }
1134
+ /**
1135
+ * Реакция транспорта на ответ `401`.
1136
+ *
1137
+ * @returns `true`, если токен обновлён и запрос имеет смысл повторить
1138
+ */
1139
+ async onUnauthorized() {
1140
+ try {
1141
+ const token = await this.#refreshDeduplicated();
1142
+ if (token !== null) return true;
1143
+ this.#emitter.emit("authError", { error: this.#noRefreshSessionError() });
1144
+ return false;
1145
+ } catch (error) {
1146
+ this.#emitter.emit("authError", { error });
1147
+ return false;
1148
+ }
1149
+ }
1150
+ /** Ошибка «сессию продлить нечем» — одна и та же для `refresh()` и для реакции на 401. */
1151
+ #noRefreshSessionError() {
1152
+ return new ItdAuthError({
1153
+ status: 401,
1154
+ code: "SESSION_EXPIRED",
1155
+ 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",
1156
+ method: "POST",
1157
+ path: AUTH_PATHS.refresh,
1158
+ raw: void 0
1159
+ });
1160
+ }
1161
+ /**
1162
+ * Обновляет токен доступа.
1163
+ *
1164
+ * Параллельные вызовы объединяются в один сетевой запрос.
1165
+ *
1166
+ * @throws {ItdAuthError} если обновить сессию не удалось
1167
+ */
1168
+ async refresh() {
1169
+ const token = await this.#refreshDeduplicated();
1170
+ if (token === null) throw this.#noRefreshSessionError();
1171
+ return token;
1172
+ }
1173
+ /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
1174
+ async setAccessToken(accessToken) {
1175
+ await this.#saveSession({ ...this.#session ?? {}, accessToken, obtainedAt: Date.now() });
1176
+ this.#emitter.emit("tokens", { accessToken });
1177
+ }
1178
+ /** Текущая сессия целиком. Полезно, чтобы сохранить её самому. */
1179
+ async getSession() {
1180
+ return this.#loadSession();
1181
+ }
1182
+ /** Заменяет сессию целиком. */
1183
+ async setSession(session) {
1184
+ this.#jar.deserialize(session.cookies);
1185
+ await this.#saveSession(session);
1186
+ }
1187
+ /** Забывает сессию и cookie. Сетевой запрос не выполняется. */
1188
+ async clear() {
1189
+ this.#session = null;
1190
+ this.#jar.clear();
1191
+ await this.#config.storage.clear();
1192
+ this.#emitter.emit("signOut", void 0);
1193
+ }
1194
+ async #loadSession() {
1195
+ if (this.#session !== void 0) return this.#session;
1196
+ const stored = await this.#config.storage.get() ?? null;
1197
+ if (stored?.cookies) this.#jar.deserialize(stored.cookies);
1198
+ const fromConfig = this.#sessionFromConfig(this.#config.auth);
1199
+ this.#session = stored && fromConfig ? {
1200
+ ...stored,
1201
+ accessToken: stored.accessToken ?? fromConfig.accessToken,
1202
+ refreshToken: stored.refreshToken ?? fromConfig.refreshToken
1203
+ } : stored ?? fromConfig;
1204
+ return this.#session;
1205
+ }
1206
+ #sessionFromConfig(auth) {
1207
+ if (!auth) return null;
1208
+ if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
1209
+ if ("accessToken" in auth) {
1210
+ return {
1211
+ accessToken: auth.accessToken,
1212
+ refreshToken: auth.refreshToken,
1213
+ obtainedAt: Date.now()
1214
+ };
1215
+ }
1216
+ return null;
1217
+ }
1218
+ async #saveSession(session) {
1219
+ const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
1220
+ const next = { ...session, ...cookies?.length ? { cookies } : {} };
1221
+ this.#session = next;
1222
+ await this.#config.storage.set(next);
1223
+ }
1224
+ /**
1225
+ * Обновление с дедупликацией.
1226
+ *
1227
+ * Все, кто пришёл, пока обновление уже идёт, получают его результат, а не запускают своё.
1228
+ */
1229
+ #refreshDeduplicated() {
1230
+ if (this.#refreshing) return this.#refreshing;
1231
+ const promise = this.#performRefresh().finally(() => {
1232
+ this.#refreshing = null;
1233
+ });
1234
+ this.#refreshing = promise;
1235
+ return promise;
1236
+ }
1237
+ async #performRefresh() {
1238
+ await this.#loadSession();
1239
+ if (!this.hasRefreshSession()) {
1240
+ return this.#reloginOrNull();
1241
+ }
1242
+ try {
1243
+ const payload = await this.#http.request({
1244
+ method: "POST",
1245
+ path: AUTH_PATHS.refresh,
1246
+ // Обновление опирается на cookie, а не на устаревший Bearer.
1247
+ skipAuth: true,
1248
+ // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1249
+ skipAuthRefresh: true,
1250
+ ...this.#session?.refreshToken ? { body: { refreshToken: this.#session.refreshToken } } : {}
1251
+ });
1252
+ const accessToken = readAccessToken(payload);
1253
+ if (!accessToken) return this.#reloginOrNull();
1254
+ await this.#saveSession({
1255
+ ...this.#session ?? {},
1256
+ accessToken,
1257
+ obtainedAt: Date.now()
1258
+ });
1259
+ this.#emitter.emit("tokens", { accessToken });
1260
+ return accessToken;
1261
+ } catch (error) {
1262
+ if (error instanceof ItdApiError) {
1263
+ this.#session = null;
1264
+ await this.#config.storage.clear();
1265
+ return this.#reloginOrNull();
1266
+ }
1267
+ throw error;
1268
+ }
1269
+ }
1270
+ /** Повторный вход, если разрешён настройкой и есть логин с паролем. */
1271
+ async #reloginOrNull() {
1272
+ const auth = this.#config.auth;
1273
+ if (!this.#config.reloginOnRefreshFailure || !auth || typeof auth !== "object" || !("email" in auth)) {
1274
+ return null;
1275
+ }
1276
+ try {
1277
+ return await this.#signInWithCredentials(auth.email, auth.password);
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+ /**
1283
+ * Вход по логину и паролю.
1284
+ *
1285
+ * Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
1286
+ * приводить к нескольким попыткам входа и блокировке аккаунта.
1287
+ */
1288
+ #signInWithCredentials(email, password) {
1289
+ if (this.#signingIn) return this.#signingIn;
1290
+ const promise = this.#performSignIn(email, password).finally(() => {
1291
+ this.#signingIn = null;
1292
+ });
1293
+ this.#signingIn = promise;
1294
+ return promise;
1295
+ }
1296
+ async #performSignIn(email, password) {
1297
+ const payload = await this.#http.request({
1298
+ method: "POST",
1299
+ path: AUTH_PATHS.signIn,
1300
+ body: { email, password },
1301
+ skipAuth: true,
1302
+ skipAuthRefresh: true
1303
+ });
1304
+ const accessToken = readAccessToken(payload);
1305
+ if (!accessToken) {
1306
+ throw new ItdConfigError(
1307
+ "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u043A\u043E\u0434\u043E\u043C \u0438\u0437 \u043F\u0438\u0441\u044C\u043C\u0430. \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0432\u0445\u043E\u0434 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D: \u0432\u043E\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435\u0441\u044C itd.auth.signInWithOtp() \u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043D\u044B\u0439 accessToken \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044E \u043A\u043B\u0438\u0435\u043D\u0442\u0430."
1308
+ );
1309
+ }
1310
+ await this.#saveSession({ accessToken, obtainedAt: Date.now() });
1311
+ this.#emitter.emit("tokens", { accessToken });
1312
+ this.#emitter.emit("signIn", { accessToken });
1313
+ return accessToken;
1314
+ }
1315
+ };
1316
+
1317
+ // src/core/runtime.ts
1318
+ function detectRuntime() {
1319
+ const nav = globalThis.navigator;
1320
+ if (nav?.product === "ReactNative") return "react-native";
1321
+ if (typeof document !== "undefined") return "browser";
1322
+ return "server";
1323
+ }
1324
+ function shouldUseCookieJar(mode) {
1325
+ if (mode === "browser") return false;
1326
+ if (mode === "server") return true;
1327
+ return detectRuntime() === "server";
1328
+ }
1329
+ function shouldSendCredentials(mode) {
1330
+ if (mode === "browser") return true;
1331
+ if (mode === "server") return false;
1332
+ return detectRuntime() === "browser";
1333
+ }
1334
+ function resolveFetch(custom) {
1335
+ if (custom) return custom;
1336
+ if (typeof globalThis.fetch === "function") {
1337
+ return globalThis.fetch.bind(globalThis);
1338
+ }
1339
+ throw new ItdConfigError(
1340
+ "\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."
1341
+ );
1342
+ }
1343
+ function supportsStreamingBody() {
1344
+ return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1345
+ }
1346
+ function hasLocalStorage() {
1347
+ try {
1348
+ return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1349
+ } catch {
1350
+ return false;
1351
+ }
1352
+ }
1353
+
1354
+ // src/core/storage.ts
1355
+ var MemoryTokenStorage = class {
1356
+ #session = null;
1357
+ constructor(initial) {
1358
+ this.#session = initial ?? null;
1359
+ }
1360
+ get() {
1361
+ return this.#session;
1362
+ }
1363
+ set(session) {
1364
+ this.#session = session;
1365
+ }
1366
+ clear() {
1367
+ this.#session = null;
1368
+ }
1369
+ };
1370
+ var LocalStorageTokenStorage = class {
1371
+ #key;
1372
+ #fallback = new MemoryTokenStorage();
1373
+ #available;
1374
+ /** @param key ключ в `localStorage`. По умолчанию `itd-api:session`. */
1375
+ constructor(key = "itd-api:session") {
1376
+ this.#key = key;
1377
+ this.#available = hasLocalStorage();
1378
+ }
1379
+ get() {
1380
+ if (!this.#available) return this.#fallback.get();
1381
+ try {
1382
+ const raw = globalThis.localStorage.getItem(this.#key);
1383
+ if (!raw) return null;
1384
+ const parsed = JSON.parse(raw);
1385
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ set(session) {
1391
+ if (!this.#available) {
1392
+ this.#fallback.set(session);
1393
+ return;
1394
+ }
1395
+ try {
1396
+ globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
1397
+ } catch {
1398
+ this.#fallback.set(session);
1399
+ }
1400
+ }
1401
+ clear() {
1402
+ if (!this.#available) {
1403
+ this.#fallback.clear();
1404
+ return;
1405
+ }
1406
+ try {
1407
+ globalThis.localStorage.removeItem(this.#key);
1408
+ } catch {
1409
+ this.#fallback.clear();
1410
+ }
1411
+ }
1412
+ };
1413
+ function createTokenStorage(handlers) {
1414
+ return handlers;
1415
+ }
1416
+
1417
+ // src/core/url.ts
1418
+ function buildQuery(params) {
1419
+ if (!params) return "";
1420
+ const search = new URLSearchParams();
1421
+ for (const [key, value] of Object.entries(params)) {
1422
+ if (value === void 0 || value === null) continue;
1423
+ if (Array.isArray(value)) {
1424
+ for (const item of value) {
1425
+ if (item === void 0 || item === null) continue;
1426
+ search.append(key, String(item));
1427
+ }
1428
+ continue;
1429
+ }
1430
+ search.append(key, String(value));
1431
+ }
1432
+ const query = search.toString();
1433
+ return query ? `?${query}` : "";
1434
+ }
1435
+ function encodePathSegment(value, name = "\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043F\u0443\u0442\u0438") {
1436
+ if (typeof value !== "string" || value.trim() === "") {
1437
+ throw new ItdConfigError(
1438
+ `${name} \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, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${JSON.stringify(value)}`
1439
+ );
1440
+ }
1441
+ return encodeURIComponent(value);
1442
+ }
1443
+ function joinUrl(baseUrl, path) {
1444
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
1445
+ const suffix = path.startsWith("/") ? path : `/${path}`;
1446
+ return `${base}${suffix}`;
1447
+ }
1448
+ function normalizeBaseUrl(baseUrl) {
1449
+ let parsed;
1450
+ try {
1451
+ parsed = new URL(baseUrl);
1452
+ } catch {
1453
+ throw new ItdConfigError(
1454
+ `baseUrl \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u044B\u043C URL, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${JSON.stringify(baseUrl)}`
1455
+ );
1456
+ }
1457
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1458
+ throw new ItdConfigError(
1459
+ `baseUrl \u0434\u043E\u043B\u0436\u0435\u043D \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C http \u0438\u043B\u0438 https, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${parsed.protocol}`
1460
+ );
1461
+ }
1462
+ return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
1463
+ }
1464
+
1465
+ // src/core/config.ts
1466
+ var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1467
+ var DEFAULT_TIMEOUT = 3e4;
1468
+ var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1469
+ function requirePositive(value, name) {
1470
+ if (!Number.isFinite(value) || value < 0) {
1471
+ throw new ItdConfigError(`${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`);
1472
+ }
1473
+ return value;
1474
+ }
1475
+ function consoleLogger() {
1476
+ return {
1477
+ debug: (message, ...args) => console.debug(`[itd-api] ${message}`, ...args),
1478
+ info: (message, ...args) => console.info(`[itd-api] ${message}`, ...args),
1479
+ warn: (message, ...args) => console.warn(`[itd-api] ${message}`, ...args),
1480
+ error: (message, ...args) => console.error(`[itd-api] ${message}`, ...args)
1481
+ };
1482
+ }
1483
+ function resolveRetry(retry) {
1484
+ if (retry === false) return void 0;
1485
+ const options = retry ?? {};
1486
+ const attempts = options.attempts ?? 3;
1487
+ if (!Number.isInteger(attempts) || attempts < 1) {
1488
+ throw new ItdConfigError(`retry.attempts \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C \u043E\u0442 1, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${attempts}`);
1489
+ }
1490
+ const jitter = options.jitter ?? 0.3;
1491
+ if (jitter < 0 || jitter > 1) {
1492
+ throw new ItdConfigError(`retry.jitter \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 0\u20261, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${jitter}`);
1493
+ }
1494
+ if (attempts === 1) return void 0;
1495
+ return {
1496
+ attempts,
1497
+ baseDelay: requirePositive(options.baseDelay ?? 500, "retry.baseDelay"),
1498
+ maxDelay: requirePositive(options.maxDelay ?? 3e4, "retry.maxDelay"),
1499
+ jitter,
1500
+ retryWrites: options.retryWrites ?? false,
1501
+ shouldRetry: options.shouldRetry
1502
+ };
1503
+ }
1504
+ function resolveRateLimit(rateLimit) {
1505
+ if (rateLimit === false) return void 0;
1506
+ const defaults = {
1507
+ concurrency: 6,
1508
+ rps: void 0,
1509
+ retryDelays: DEFAULT_RATE_LIMIT_DELAYS,
1510
+ respectHeaders: true
1511
+ };
1512
+ if (!rateLimit) return defaults;
1513
+ const concurrency = rateLimit.concurrency ?? 6;
1514
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
1515
+ throw new ItdConfigError(
1516
+ `rateLimit.concurrency \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C \u043E\u0442 1, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${concurrency}`
1517
+ );
1518
+ }
1519
+ if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) {
1520
+ throw new ItdConfigError(
1521
+ `rateLimit.rps \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${rateLimit.rps}`
1522
+ );
1523
+ }
1524
+ const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
1525
+ for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
1526
+ return {
1527
+ concurrency,
1528
+ rps: rateLimit.rps,
1529
+ retryDelays,
1530
+ respectHeaders: rateLimit.respectHeaders ?? true
1531
+ };
1532
+ }
1533
+ function validateAuth(auth) {
1534
+ if (auth === void 0) return void 0;
1535
+ if (typeof auth === "string") {
1536
+ if (auth.trim() === "") {
1537
+ throw new ItdConfigError("auth: \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u0430 \u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0432\u043C\u0435\u0441\u0442\u043E accessToken");
1538
+ }
1539
+ return auth;
1540
+ }
1541
+ if (typeof auth !== "object" || auth === null) {
1542
+ throw new ItdConfigError(
1543
+ `auth \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \u0441 \u0442\u043E\u043A\u0435\u043D\u043E\u043C \u0438\u043B\u0438 \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${typeof auth}`
1544
+ );
1545
+ }
1546
+ if ("getToken" in auth) {
1547
+ if (typeof auth.getToken !== "function") {
1548
+ throw new ItdConfigError("auth.getToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
1549
+ }
1550
+ return auth;
1551
+ }
1552
+ if ("accessToken" in auth) {
1553
+ if (typeof auth.accessToken !== "string" || auth.accessToken.trim() === "") {
1554
+ throw new ItdConfigError("auth.accessToken \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");
1555
+ }
1556
+ return auth;
1557
+ }
1558
+ if ("email" in auth || "password" in auth) {
1559
+ const { email, password } = auth;
1560
+ if (typeof email !== "string" || email.trim() === "") {
1561
+ 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");
1562
+ }
1563
+ if (typeof password !== "string" || password === "") {
1564
+ 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");
1565
+ }
1566
+ return auth;
1567
+ }
1568
+ throw new ItdConfigError(
1569
+ "auth \u043D\u0435 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D. \u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0441 accessToken \u043B\u0438\u0431\u043E \u043E\u0431\u044A\u0435\u043A\u0442 { accessToken }, { email, password } \u0438\u043B\u0438 { getToken }"
1570
+ );
1571
+ }
1572
+ function resolveConfig(options = {}) {
1573
+ const mode = options.mode ?? "auto";
1574
+ if (mode !== "auto" && mode !== "browser" && mode !== "server") {
1575
+ 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}`);
1576
+ }
1577
+ const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1578
+ return {
1579
+ baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1580
+ auth: validateAuth(options.auth),
1581
+ storage: options.storage ?? new MemoryTokenStorage(),
1582
+ autoRefresh: options.autoRefresh ?? true,
1583
+ reloginOnRefreshFailure: options.reloginOnRefreshFailure ?? true,
1584
+ fetch: resolveFetch(options.fetch),
1585
+ timeout,
1586
+ retry: resolveRetry(options.retry),
1587
+ rateLimit: resolveRateLimit(options.rateLimit),
1588
+ hooks: options.hooks ?? {},
1589
+ logger: options.logger === true ? consoleLogger() : options.logger || void 0,
1590
+ headers: { ...options.headers },
1591
+ mode,
1592
+ useCookieJar: shouldUseCookieJar(mode),
1593
+ sendCredentials: shouldSendCredentials(mode)
1594
+ };
1595
+ }
1596
+
1597
+ // src/core/error-factory.ts
1598
+ function isRecord(value) {
1599
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1600
+ }
1601
+ function asString(value) {
1602
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1603
+ }
1604
+ function collectFieldErrors(source) {
1605
+ const result = {};
1606
+ const errors = source.errors;
1607
+ if (isRecord(errors)) {
1608
+ for (const [field, value] of Object.entries(errors)) {
1609
+ if (Array.isArray(value)) {
1610
+ const messages = value.filter((item) => typeof item === "string");
1611
+ if (messages.length > 0) result[field] = messages;
1612
+ } else if (typeof value === "string") {
1613
+ result[field] = [value];
1614
+ }
1615
+ }
1616
+ }
1617
+ const violations = source.violations;
1618
+ if (Array.isArray(violations)) {
1619
+ for (const violation of violations) {
1620
+ if (!isRecord(violation)) continue;
1621
+ const field = asString(violation.field) ?? asString(violation.property);
1622
+ const message = asString(violation.message);
1623
+ if (!field || !message) continue;
1624
+ const existing = result[field];
1625
+ if (existing) existing.push(message);
1626
+ else result[field] = [message];
1627
+ }
1628
+ }
1629
+ return result;
1630
+ }
1631
+ function parseErrorBody(body, status, statusText = "") {
1632
+ const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
1633
+ if (typeof body === "string") {
1634
+ return {
1635
+ code: "UNKNOWN_ERROR",
1636
+ message: asString(body.trim()) ?? fallbackMessage,
1637
+ detail: void 0,
1638
+ title: void 0,
1639
+ fieldErrors: {},
1640
+ userId: void 0
1641
+ };
1642
+ }
1643
+ if (!isRecord(body)) {
1644
+ return {
1645
+ code: "UNKNOWN_ERROR",
1646
+ message: fallbackMessage,
1647
+ detail: void 0,
1648
+ title: void 0,
1649
+ fieldErrors: {},
1650
+ userId: void 0
1651
+ };
1652
+ }
1653
+ if (body.type === "validation") {
1654
+ const target = asString(body.on);
1655
+ return {
1656
+ code: "VALIDATION_ERROR",
1657
+ message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
1658
+ detail: void 0,
1659
+ title: void 0,
1660
+ fieldErrors: {},
1661
+ userId: void 0
1662
+ };
1663
+ }
1664
+ const inner = isRecord(body.error) ? body.error : body;
1665
+ const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
1666
+ asString(body.error) ?? fallbackMessage;
1667
+ return {
1668
+ code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
1669
+ message,
1670
+ detail: asString(inner.detail),
1671
+ title: asString(inner.title),
1672
+ fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
1673
+ userId: asString(inner.userId) ?? asString(body.userId)
1674
+ };
1675
+ }
1676
+ function parseRetryAfter(header, now = Date.now()) {
1677
+ if (!header) return void 0;
1678
+ const seconds = Number(header);
1679
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
1680
+ const date = Date.parse(header);
1681
+ if (Number.isFinite(date)) return Math.max(0, date - now);
1682
+ return void 0;
1683
+ }
1684
+ function readIntHeader(headers, name) {
1685
+ const raw = headers?.get(name);
1686
+ if (raw === null || raw === void 0) return void 0;
1687
+ const value = Number.parseInt(raw, 10);
1688
+ return Number.isFinite(value) ? value : void 0;
1689
+ }
1690
+ function readRateLimit(headers) {
1691
+ return {
1692
+ limit: readIntHeader(headers, "x-ratelimit-limit"),
1693
+ remaining: readIntHeader(headers, "x-ratelimit-remaining")
1694
+ };
1695
+ }
1696
+ var REQUEST_ID_HEADERS = ["x-request-id", "x-requestid", "request-id", "x-correlation-id"];
1697
+ function getRequestId(headers) {
1698
+ if (!headers) return void 0;
1699
+ for (const name of REQUEST_ID_HEADERS) {
1700
+ const value = headers.get(name);
1701
+ if (value) return value;
1702
+ }
1703
+ return void 0;
1704
+ }
1705
+ var CODE_TO_CLASS = {
1706
+ VALIDATION_ERROR: ItdValidationError,
1707
+ RATE_LIMIT_EXCEEDED: ItdRateLimitError,
1708
+ UNAUTHORIZED: ItdAuthError,
1709
+ SESSION_EXPIRED: ItdAuthError,
1710
+ SESSION_REVOKED: ItdAuthError,
1711
+ SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
1712
+ ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
1713
+ ACCESS_DENIED: ItdForbiddenError,
1714
+ ENTITY_NOT_FOUND: ItdNotFoundError,
1715
+ // Сервер отвечает именно так: `{ error: { code: 'NOT_FOUND', message: 'Post not found' } }`.
1716
+ NOT_FOUND: ItdNotFoundError,
1717
+ ENTITY_ALREADY_EXISTS: ItdConflictError
1718
+ };
1719
+ function classByStatus(status) {
1720
+ if (status === 401) return ItdAuthError;
1721
+ if (status === 403) return ItdForbiddenError;
1722
+ if (status === 404) return ItdNotFoundError;
1723
+ if (status === 409) return ItdConflictError;
1724
+ if (status === 422) return ItdValidationError;
1725
+ if (status === 429) return ItdRateLimitError;
1726
+ if (status >= 500) return ItdServerError;
1727
+ return ItdApiError;
1728
+ }
1729
+ function createApiError(context) {
1730
+ const parsed = parseErrorBody(context.body, context.status, context.statusText);
1731
+ const rateLimit = readRateLimit(context.headers);
1732
+ const init = {
1733
+ rateLimit: rateLimit.limit,
1734
+ rateLimitRemaining: rateLimit.remaining,
1735
+ status: context.status,
1736
+ code: parsed.code,
1737
+ message: parsed.message,
1738
+ detail: parsed.detail,
1739
+ title: parsed.title,
1740
+ fieldErrors: parsed.fieldErrors,
1741
+ requestId: getRequestId(context.headers),
1742
+ method: context.method,
1743
+ path: context.path,
1744
+ raw: context.body,
1745
+ response: context.response,
1746
+ retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
1747
+ };
1748
+ if (parsed.code === "PHONE_VERIFICATION_REQUIRED") {
1749
+ return new ItdPhoneVerificationError({ ...init, userId: parsed.userId });
1750
+ }
1751
+ const Ctor = CODE_TO_CLASS[parsed.code] ?? classByStatus(context.status);
1752
+ return new Ctor(init);
1753
+ }
1754
+
1755
+ // src/core/redact.ts
1756
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1757
+ var SECRET_FIELDS = /* @__PURE__ */ new Set([
1758
+ "password",
1759
+ "oldpassword",
1760
+ "newpassword",
1761
+ "accesstoken",
1762
+ "refreshtoken",
1763
+ "flowtoken",
1764
+ "token",
1765
+ "otp"
1766
+ ]);
1767
+ function maskSecret(value) {
1768
+ if (value.length <= 8) return "\u2026";
1769
+ return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1770
+ }
1771
+ function redactHeaders(headers) {
1772
+ const result = {};
1773
+ headers.forEach((value, name) => {
1774
+ if (SECRET_HEADERS.has(name.toLowerCase())) {
1775
+ const spaceAt = value.indexOf(" ");
1776
+ result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1777
+ return;
1778
+ }
1779
+ result[name] = value;
1780
+ });
1781
+ return result;
1782
+ }
1783
+ function redactBody(body) {
1784
+ if (body === null || body === void 0) return body;
1785
+ if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1786
+ if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1787
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1788
+ if (Array.isArray(body)) return body.map(redactBody);
1789
+ if (typeof body === "object") {
1790
+ const result = {};
1791
+ for (const [key, value] of Object.entries(body)) {
1792
+ result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1793
+ }
1794
+ return result;
1795
+ }
1796
+ return body;
1797
+ }
1798
+
1799
+ // src/core/unwrap.ts
1800
+ function unwrapData(body) {
1801
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1802
+ const keys = Object.keys(body);
1803
+ if (keys.length !== 1 || keys[0] !== "data") return body;
1804
+ return body.data;
1805
+ }
1806
+ function pickArray(source, field) {
1807
+ if (typeof source !== "object" || source === null) return [];
1808
+ const value = source[field];
1809
+ return Array.isArray(value) ? value : [];
1810
+ }
1811
+ function pickObject(source, field) {
1812
+ if (typeof source !== "object" || source === null) return void 0;
1813
+ const value = source[field];
1814
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1815
+ return value;
1816
+ }
1817
+ function pickBoolean(source, field, fallback = false) {
1818
+ if (typeof source !== "object" || source === null) return fallback;
1819
+ const value = source[field];
1820
+ return typeof value === "boolean" ? value : fallback;
1821
+ }
1822
+ function pickNumber(source, field, fallback) {
1823
+ if (typeof source !== "object" || source === null) return fallback;
1824
+ const value = source[field];
1825
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1826
+ }
1827
+ function pickString(source, field) {
1828
+ if (typeof source !== "object" || source === null) return void 0;
1829
+ const value = source[field];
1830
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1831
+ }
1832
+
1833
+ // src/core/http.ts
1834
+ function sleep(ms) {
1835
+ return new Promise((resolve) => setTimeout(resolve, ms));
1836
+ }
1837
+ function setHeader(headers, name, value) {
1838
+ try {
1839
+ headers.set(name, value);
1840
+ } catch {
1841
+ throw new ItdConfigError(
1842
+ `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 ${name} \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0432\u043D\u0435 latin1. HTTP-\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438 \u043D\u0435 \u043C\u043E\u0433\u0443\u0442 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u043A\u0438\u0440\u0438\u043B\u043B\u0438\u0446\u0443 \u2014 \u0437\u0430\u043A\u043E\u0434\u0438\u0440\u0443\u0439\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u0447\u0435\u0440\u0435\u0437 encodeURIComponent.`
1843
+ );
1844
+ }
1845
+ }
1846
+ function isRawBody(body) {
1847
+ if (typeof body !== "object" || body === null) return typeof body === "string";
1848
+ 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);
1849
+ }
1850
+ async function readBody(response) {
1851
+ if (response.status === 204 || response.status === 205) return void 0;
1852
+ if (response.headers.get("content-length") === "0") return void 0;
1853
+ const contentType = response.headers.get("content-type") ?? "";
1854
+ if (contentType.includes("json")) {
1855
+ const text2 = await response.text();
1856
+ if (text2 === "") return void 0;
1857
+ try {
1858
+ return JSON.parse(text2);
1859
+ } catch {
1860
+ return text2;
1861
+ }
1862
+ }
1863
+ const text = await response.text();
1864
+ return text === "" ? void 0 : text;
1865
+ }
1866
+ function createAbortBundle(userSignal, timeout) {
1867
+ const controller = new AbortController();
1868
+ let timedOut = false;
1869
+ const onUserAbort = () => controller.abort(userSignal?.reason);
1870
+ if (userSignal) {
1871
+ if (userSignal.aborted) controller.abort(userSignal.reason);
1872
+ else userSignal.addEventListener("abort", onUserAbort, { once: true });
1873
+ }
1874
+ const timer = timeout > 0 ? setTimeout(() => {
1875
+ timedOut = true;
1876
+ controller.abort();
1877
+ }, timeout) : void 0;
1878
+ return {
1879
+ signal: controller.signal,
1880
+ timedOut: () => timedOut,
1881
+ cleanup: () => {
1882
+ if (timer !== void 0) clearTimeout(timer);
1883
+ userSignal?.removeEventListener("abort", onUserAbort);
1884
+ }
1885
+ };
1886
+ }
1887
+ var HttpClient = class {
1888
+ #config;
1889
+ #collaborators;
1890
+ constructor(config, collaborators = {}) {
1891
+ this.#config = config;
1892
+ this.#collaborators = collaborators;
1893
+ }
1894
+ /** Базовый URL, к которому обращается клиент. */
1895
+ get baseUrl() {
1896
+ return this.#config.baseUrl;
1897
+ }
1898
+ /**
1899
+ * Подключает недостающие части конвейера.
1900
+ *
1901
+ * Нужно из-за кольцевой зависимости: слой авторизации сам выполняет запросы, поэтому
1902
+ * не может быть передан в конструктор до создания транспорта.
1903
+ */
1904
+ setCollaborators(collaborators) {
1905
+ this.#collaborators = { ...this.#collaborators, ...collaborators };
1906
+ }
1907
+ /**
1908
+ * Выполняет запрос к API.
1909
+ *
1910
+ * @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
1911
+ * @throws {ItdApiError} если сервер ответил статусом ≥ 400
1912
+ * @throws {ItdTimeoutError} если истёк таймаут
1913
+ * @throws {ItdAbortError} если запрос отменён через `signal`
1914
+ * @throws {ItdNetworkError} если запрос не дошёл до сервера
1915
+ */
1916
+ async request(options) {
1917
+ const task = () => this.#withRetries(options);
1918
+ return this.#collaborators.schedule ? this.#collaborators.schedule(task) : task();
1919
+ }
1920
+ async #withRetries(options) {
1921
+ const method = options.method.toUpperCase();
1922
+ for (let attempt = 1; ; attempt++) {
1923
+ try {
1924
+ return await this.#attempt(options, attempt);
1925
+ } catch (error) {
1926
+ const delay = this.#collaborators.nextRetryDelay?.(error, attempt, method);
1927
+ if (delay === void 0) throw error;
1928
+ await this.#config.hooks.onRetry?.({
1929
+ method,
1930
+ path: options.path,
1931
+ url: this.#buildUrl(options),
1932
+ headers: new Headers(),
1933
+ attempt,
1934
+ error,
1935
+ delay
1936
+ });
1937
+ this.#config.logger?.debug(
1938
+ `\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${options.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
1939
+ );
1940
+ await sleep(delay);
1941
+ }
1942
+ }
1943
+ }
1944
+ #buildUrl(options) {
1945
+ return joinUrl(this.#config.baseUrl, options.path) + buildQuery(options.query);
1946
+ }
1947
+ async #buildHeaders(options, url) {
1948
+ const headers = new Headers();
1949
+ headers.set("Accept", "application/json");
1950
+ for (const [name, value] of Object.entries(this.#config.headers))
1951
+ setHeader(headers, name, value);
1952
+ if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
1953
+ const auth = await this.#collaborators.getAuthHeaders();
1954
+ for (const [name, value] of Object.entries(auth)) setHeader(headers, name, value);
1955
+ }
1956
+ if (this.#config.useCookieJar && this.#collaborators.getCookieHeader) {
1957
+ const cookie = this.#collaborators.getCookieHeader(url);
1958
+ if (cookie) setHeader(headers, "Cookie", cookie);
1959
+ }
1960
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
1961
+ setHeader(headers, name, value);
1962
+ }
1963
+ return headers;
1964
+ }
1965
+ async #attempt(options, attempt) {
1966
+ const method = options.method.toUpperCase();
1967
+ const url = this.#buildUrl(options);
1968
+ const headers = await this.#buildHeaders(options, url);
1969
+ let body;
1970
+ if (options.body !== void 0 && options.body !== null) {
1971
+ if (isRawBody(options.body)) {
1972
+ body = options.body;
1973
+ } else {
1974
+ body = JSON.stringify(options.body);
1975
+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
1976
+ }
1977
+ }
1978
+ const context = { method, path: options.path, url, headers, attempt };
1979
+ await this.#config.hooks.onRequest?.(context);
1980
+ const timeout = options.timeout ?? this.#config.timeout;
1981
+ const abort = createAbortBundle(options.signal, timeout);
1982
+ const startedAt = Date.now();
1983
+ this.#config.logger?.debug(`\u2192 ${method} ${options.path}`, {
1984
+ headers: redactHeaders(headers),
1985
+ body: redactBody(options.body)
1986
+ });
1987
+ let response;
1988
+ try {
1989
+ response = await this.#config.fetch(url, {
1990
+ method,
1991
+ headers,
1992
+ signal: abort.signal,
1993
+ ...body !== void 0 ? { body } : {},
1994
+ ...this.#config.sendCredentials ? { credentials: "include" } : {}
1995
+ });
1996
+ } catch (error) {
1997
+ const duration2 = Date.now() - startedAt;
1998
+ const failure = this.#toTransportError(error, abort, options, method, timeout);
1999
+ await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2000
+ this.#config.logger?.warn(`\xD7 ${method} ${options.path} (${duration2} \u043C\u0441): ${failure.message}`);
2001
+ throw failure;
2002
+ } finally {
2003
+ abort.cleanup();
2004
+ }
2005
+ const duration = Date.now() - startedAt;
2006
+ if (this.#collaborators.onRateLimit) {
2007
+ const { limit, remaining } = readRateLimit(response.headers);
2008
+ this.#collaborators.onRateLimit(limit, remaining);
2009
+ }
2010
+ if (this.#config.useCookieJar) this.#collaborators.saveCookies?.(url, response);
2011
+ const payload = await readBody(response);
2012
+ if (!response.ok) {
2013
+ if (response.status === 401 && !options.skipAuthRefresh && this.#config.autoRefresh && this.#collaborators.onUnauthorized) {
2014
+ const refreshed = await this.#collaborators.onUnauthorized();
2015
+ if (refreshed) {
2016
+ this.#config.logger?.debug(`\u0442\u043E\u043A\u0435\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E ${method} ${options.path}`);
2017
+ return this.#attempt({ ...options, skipAuthRefresh: true }, attempt);
2018
+ }
2019
+ }
2020
+ const error = createApiError({
2021
+ method,
2022
+ path: options.path,
2023
+ status: response.status,
2024
+ statusText: response.statusText,
2025
+ headers: response.headers,
2026
+ response,
2027
+ body: payload
2028
+ });
2029
+ await this.#config.hooks.onError?.({ ...context, duration, error });
2030
+ this.#config.logger?.warn(
2031
+ `\u2190 ${response.status} ${method} ${options.path} (${duration} \u043C\u0441): ${error.message}`
2032
+ );
2033
+ throw error;
2034
+ }
2035
+ await this.#config.hooks.onResponse?.({
2036
+ ...context,
2037
+ status: response.status,
2038
+ duration,
2039
+ response
2040
+ });
2041
+ this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${options.path} (${duration} \u043C\u0441)`);
2042
+ return options.raw ? payload : unwrapData(payload);
2043
+ }
2044
+ /** Превращает исключение `fetch` в понятную ошибку библиотеки. */
2045
+ #toTransportError(error, abort, options, method, timeout) {
2046
+ const aborted = error instanceof Error && error.name === "AbortError";
2047
+ if (aborted && abort.timedOut()) {
2048
+ return new ItdTimeoutError({ timeout, method, path: options.path });
2049
+ }
2050
+ if (aborted) {
2051
+ return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${options.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
2052
+ }
2053
+ return new ItdNetworkError(
2054
+ `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${options.path}: ${error instanceof Error ? error.message : String(error)}`,
2055
+ { method, path: options.path, cause: error }
2056
+ );
2057
+ }
2058
+ };
2059
+
2060
+ // src/core/rate-limit.ts
2061
+ var RequestQueue = class {
2062
+ #concurrency;
2063
+ /** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
2064
+ #minGap;
2065
+ #waiting = [];
2066
+ #active = 0;
2067
+ /** Момент, раньше которого следующий запрос стартовать не должен. */
2068
+ #nextSlot = 0;
2069
+ #timer;
2070
+ constructor(options) {
2071
+ this.#concurrency = options.concurrency;
2072
+ this.#minGap = options.rps ? 1e3 / options.rps : 0;
2073
+ }
2074
+ /** Сколько задач выполняется прямо сейчас. */
2075
+ get active() {
2076
+ return this.#active;
2077
+ }
2078
+ /** Сколько задач ждёт очереди. */
2079
+ get pending() {
2080
+ return this.#waiting.length;
2081
+ }
2082
+ /**
2083
+ * Ставит задачу в очередь.
2084
+ *
2085
+ * @returns результат задачи; ошибка задачи пробрасывается без изменений
2086
+ */
2087
+ schedule(task) {
2088
+ return new Promise((resolve, reject) => {
2089
+ const run = () => {
2090
+ this.#active += 1;
2091
+ task().then(resolve, reject).finally(() => {
2092
+ this.#active -= 1;
2093
+ this.#drain();
2094
+ });
2095
+ };
2096
+ this.#waiting.push({ run });
2097
+ this.#drain();
2098
+ });
2099
+ }
2100
+ /**
2101
+ * Придерживает всю очередь на заданное время.
2102
+ *
2103
+ * Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
2104
+ * а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
2105
+ */
2106
+ pause(ms) {
2107
+ if (ms <= 0) return;
2108
+ this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
2109
+ }
2110
+ /** Запускает столько ожидающих задач, сколько позволяют ограничения. */
2111
+ #drain() {
2112
+ if (this.#waiting.length === 0) return;
2113
+ if (this.#active >= this.#concurrency) return;
2114
+ if (this.#timer !== void 0) return;
2115
+ const now = Date.now();
2116
+ if (this.#nextSlot > now) {
2117
+ this.#timer = setTimeout(() => {
2118
+ this.#timer = void 0;
2119
+ this.#drain();
2120
+ }, this.#nextSlot - now);
2121
+ return;
2122
+ }
2123
+ const next = this.#waiting.shift();
2124
+ if (!next) return;
2125
+ if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
2126
+ next.run();
2127
+ this.#drain();
2128
+ }
2129
+ };
2130
+
2131
+ // src/core/retry.ts
2132
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
2133
+ function isRetryable(error, method, retryWrites) {
2134
+ if (error instanceof ItdAbortError) return false;
2135
+ const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
2136
+ if (error instanceof ItdApiError) {
2137
+ if (error.status === 429) return true;
2138
+ if (error.status >= 500) return safeToRepeat;
2139
+ return false;
2140
+ }
2141
+ if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
2142
+ return false;
2143
+ }
2144
+ function backoffDelay(attempt, options, random) {
2145
+ const exponential = options.baseDelay * 2 ** (attempt - 1);
2146
+ const capped = Math.min(exponential, options.maxDelay);
2147
+ const spread = capped * options.jitter * (random() * 2 - 1);
2148
+ return Math.max(0, Math.round(capped + spread));
2149
+ }
2150
+ function createRetryScheduler(options, random = Math.random) {
2151
+ return (error, attempt, method) => {
2152
+ if (attempt >= options.attempts) return void 0;
2153
+ if (options.shouldRetry) {
2154
+ return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
2155
+ }
2156
+ if (!isRetryable(error, method, options.retryWrites)) return void 0;
2157
+ if (error instanceof ItdApiError && error.retryAfter !== void 0) {
2158
+ return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
2159
+ }
2160
+ return backoffDelay(attempt, options, random);
2161
+ };
2162
+ }
2163
+
2164
+ // src/notifications/type-map.ts
2165
+ var NOTIFICATION_TYPE_ALIASES = Object.freeze({
2166
+ like: NotificationType.PostReaction,
2167
+ comment: NotificationType.PostComment,
2168
+ comment_like: NotificationType.CommentReaction,
2169
+ reply: NotificationType.CommentReply,
2170
+ repost: NotificationType.PostRepost,
2171
+ mention: NotificationType.PostMention
2172
+ });
2173
+ var KNOWN_TYPES = new Set(Object.values(NotificationType));
2174
+ function canonicalNotificationType(rawType) {
2175
+ return NOTIFICATION_TYPE_ALIASES[rawType] ?? rawType;
2176
+ }
2177
+ function isKnownNotificationType(type) {
2178
+ return KNOWN_TYPES.has(canonicalNotificationType(type));
2179
+ }
2180
+
2181
+ // src/notifications/normalize.ts
2182
+ function isRecord2(value) {
2183
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2184
+ }
2185
+ function asString2(value) {
2186
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2187
+ }
2188
+ function asActor(value) {
2189
+ if (!isRecord2(value)) return void 0;
2190
+ const id = asString2(value.id);
2191
+ if (!id) return void 0;
2192
+ return {
2193
+ id,
2194
+ username: asString2(value.username) ?? "",
2195
+ displayName: asString2(value.displayName) ?? "",
2196
+ avatar: asString2(value.avatar) ?? "",
2197
+ ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2198
+ ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2199
+ };
2200
+ }
2201
+ function readActors(source) {
2202
+ if (Array.isArray(source.actors)) {
2203
+ return source.actors.map(asActor).filter((actor) => actor !== void 0);
2204
+ }
2205
+ const single = asActor(source.actor);
2206
+ return single ? [single] : [];
2207
+ }
2208
+ function normalizeNotification(input) {
2209
+ const source = isRecord2(input) ? input : {};
2210
+ const payload = isRecord2(source.payload) ? source.payload : source;
2211
+ const rawType = asString2(payload.type) ?? asString2(source.type) ?? "";
2212
+ const createdAt = asString2(payload.createdAt) ?? asString2(source.createdAt) ?? "";
2213
+ const readAt = asString2(payload.readAt) ?? asString2(source.readAt);
2214
+ const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2215
+ const subjectId = asString2(payload.subjectId);
2216
+ const targetId = asString2(payload.targetId);
2217
+ const subjectIsComment = payload.subjectType === "comment";
2218
+ return {
2219
+ id: asString2(payload.id) ?? asString2(source.id) ?? "",
2220
+ type: canonicalNotificationType(rawType),
2221
+ rawType,
2222
+ entityId: asString2(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2223
+ parentEntityId: asString2(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2224
+ isRead,
2225
+ actors: readActors(payload),
2226
+ count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2227
+ preview: asString2(payload.entityPreview) ?? asString2(payload.preview) ?? null,
2228
+ ...asString2(payload.clickUrl) ? { clickUrl: asString2(payload.clickUrl) } : {},
2229
+ createdAt,
2230
+ updatedAt: asString2(payload.updatedAt) ?? readAt ?? createdAt,
2231
+ raw: input
2232
+ };
2233
+ }
2234
+ function readNotificationEvent(data) {
2235
+ const source = isRecord2(data) ? data : {};
2236
+ return {
2237
+ notification: normalizeNotification(data),
2238
+ unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
2239
+ sound: source.sound === true
2240
+ };
2241
+ }
2242
+ function readUnreadCountEvent(data) {
2243
+ if (!isRecord2(data)) return void 0;
2244
+ const payload = isRecord2(data.payload) ? data.payload : void 0;
2245
+ if (!payload) return void 0;
2246
+ return typeof payload.count === "number" ? payload.count : void 0;
2247
+ }
2248
+
2249
+ // src/realtime/transport.ts
2250
+ var UnauthorizedStreamError = class extends Error {
2251
+ constructor() {
2252
+ super("\u041F\u043E\u0442\u043E\u043A \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u043E\u0442\u0432\u0435\u0440\u0433 \u0442\u043E\u043A\u0435\u043D \u0434\u043E\u0441\u0442\u0443\u043F\u0430");
2253
+ this.name = "UnauthorizedStreamError";
2254
+ }
2255
+ };
2256
+
2257
+ // src/realtime/poll.ts
2258
+ var PollTransport = class {
2259
+ name = "poll";
2260
+ #interval;
2261
+ #limit;
2262
+ constructor(options = {}) {
2263
+ this.#interval = options.interval ?? 15e3;
2264
+ this.#limit = options.limit ?? 20;
2265
+ }
2266
+ async connect(context) {
2267
+ const seen = /* @__PURE__ */ new Set();
2268
+ let firstRun = true;
2269
+ let lastUnreadCount;
2270
+ context.onOpen();
2271
+ while (!context.signal.aborted) {
2272
+ const token = await context.getToken();
2273
+ if (!token) throw new UnauthorizedStreamError();
2274
+ const headers = new Headers({
2275
+ Accept: "application/json",
2276
+ Authorization: `Bearer ${token}`
2277
+ });
2278
+ const response = await context.fetch(
2279
+ `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`,
2280
+ { method: "GET", headers, signal: context.signal }
2281
+ );
2282
+ if (response.status === 401) throw new UnauthorizedStreamError();
2283
+ if (!response.ok) throw new Error(`\u041E\u043F\u0440\u043E\u0441 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
2284
+ const body = await response.json();
2285
+ const payload = typeof body === "object" && body !== null && "data" in body ? body.data : body;
2286
+ const items = pickArray(payload, "notifications");
2287
+ for (const item of [...items].reverse()) {
2288
+ const id = typeof item.id === "string" ? item.id : void 0;
2289
+ if (!id || seen.has(id)) continue;
2290
+ seen.add(id);
2291
+ if (!firstRun) context.onEvent({ name: "notification", data: { payload: item } });
2292
+ }
2293
+ if (seen.size > this.#limit * 2) {
2294
+ const excess = [...seen].slice(0, seen.size - this.#limit * 2);
2295
+ for (const id of excess) seen.delete(id);
2296
+ }
2297
+ const count = await this.#readCount(context, headers);
2298
+ if (count !== void 0 && count !== lastUnreadCount) {
2299
+ lastUnreadCount = count;
2300
+ context.onEvent({ name: "unread_count", data: { payload: { count } } });
2301
+ }
2302
+ firstRun = false;
2303
+ await this.#wait(context.signal);
2304
+ }
2305
+ }
2306
+ async #readCount(context, headers) {
2307
+ try {
2308
+ const response = await context.fetch(joinUrl(context.baseUrl, "/api/notifications/count"), {
2309
+ method: "GET",
2310
+ headers,
2311
+ signal: context.signal
2312
+ });
2313
+ if (!response.ok) return void 0;
2314
+ const body = await response.json();
2315
+ const payload = typeof body === "object" && body !== null && "data" in body ? body.data : body;
2316
+ return pickNumber(payload, "count", 0);
2317
+ } catch {
2318
+ return void 0;
2319
+ }
2320
+ }
2321
+ /** Ждёт следующего опроса, прерываясь при отмене. */
2322
+ #wait(signal) {
2323
+ return new Promise((resolve) => {
2324
+ const timer = setTimeout(finish, this.#interval);
2325
+ function finish() {
2326
+ clearTimeout(timer);
2327
+ signal.removeEventListener("abort", finish);
2328
+ resolve();
2329
+ }
2330
+ signal.addEventListener("abort", finish, { once: true });
2331
+ });
2332
+ }
2333
+ };
2334
+
2335
+ // src/realtime/reconnect.ts
2336
+ var RECONNECT_BACKOFF = Object.freeze([1e3, 2e3, 4e3, 8e3, 16e3, 3e4]);
2337
+ var RECONNECT_JITTER = 0.3;
2338
+ var MAX_RECONNECT_ATTEMPTS = 15;
2339
+ function reconnectDelay(attempt, options = {}, random = Math.random) {
2340
+ const backoff = options.backoff ?? RECONNECT_BACKOFF;
2341
+ const jitter = options.jitter ?? RECONNECT_JITTER;
2342
+ const base = backoff[Math.min(attempt, backoff.length - 1)] ?? 3e4;
2343
+ const spread = base * jitter * (random() * 2 - 1);
2344
+ return Math.max(0, Math.round(base + spread));
2345
+ }
2346
+
2347
+ // node_modules/eventsource-parser/dist/index.js
2348
+ var ParseError = class extends Error {
2349
+ constructor(message, options) {
2350
+ super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
2351
+ }
2352
+ };
2353
+ var LF = 10;
2354
+ var CR = 13;
2355
+ var SPACE = 32;
2356
+ function noop(_arg) {
2357
+ }
2358
+ function createParser(config) {
2359
+ if (typeof config == "function")
2360
+ throw new TypeError(
2361
+ "`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?"
2362
+ );
2363
+ const { onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize } = config, pendingFragments = [];
2364
+ let pendingFragmentsLength = 0, isFirstChunk = true, id, data = "", dataLines = 0, eventType, terminated = false;
2365
+ function feed(chunk) {
2366
+ if (terminated)
2367
+ throw new Error(
2368
+ "Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing."
2369
+ );
2370
+ if (isFirstChunk && (isFirstChunk = false, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
2371
+ const trailing2 = processLines(chunk);
2372
+ trailing2 !== "" && (pendingFragments.push(trailing2), pendingFragmentsLength = trailing2.length), checkBufferSize();
2373
+ return;
2374
+ }
2375
+ if (chunk.indexOf(`
2376
+ `) === -1 && chunk.indexOf("\r") === -1) {
2377
+ pendingFragments.push(chunk), pendingFragmentsLength += chunk.length, checkBufferSize();
2378
+ return;
2379
+ }
2380
+ pendingFragments.push(chunk);
2381
+ const input = pendingFragments.join("");
2382
+ pendingFragments.length = 0, pendingFragmentsLength = 0;
2383
+ const trailing = processLines(input);
2384
+ trailing !== "" && (pendingFragments.push(trailing), pendingFragmentsLength = trailing.length), checkBufferSize();
2385
+ }
2386
+ function checkBufferSize() {
2387
+ maxBufferSize !== void 0 && (pendingFragmentsLength + data.length <= maxBufferSize || (terminated = true, pendingFragments.length = 0, pendingFragmentsLength = 0, id = void 0, data = "", dataLines = 0, eventType = void 0, onError(
2388
+ new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {
2389
+ type: "max-buffer-size-exceeded"
2390
+ })
2391
+ )));
2392
+ }
2393
+ function processLines(chunk) {
2394
+ let searchIndex = 0;
2395
+ if (chunk.indexOf("\r") === -1) {
2396
+ let lfIndex = chunk.indexOf(`
2397
+ `, searchIndex);
2398
+ for (; lfIndex !== -1; ) {
2399
+ if (searchIndex === lfIndex) {
2400
+ dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
2401
+ `, searchIndex);
2402
+ continue;
2403
+ }
2404
+ const firstCharCode = chunk.charCodeAt(searchIndex);
2405
+ if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
2406
+ const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
2407
+ if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
2408
+ onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
2409
+ `, searchIndex);
2410
+ continue;
2411
+ }
2412
+ data = dataLines === 0 ? value : `${data}
2413
+ ${value}`, dataLines++;
2414
+ } else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(
2415
+ chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,
2416
+ lfIndex
2417
+ ) || void 0 : parseLine(chunk, searchIndex, lfIndex);
2418
+ searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
2419
+ `, searchIndex);
2420
+ }
2421
+ return chunk.slice(searchIndex);
2422
+ }
2423
+ for (; searchIndex < chunk.length; ) {
2424
+ const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
2425
+ `, searchIndex);
2426
+ let lineEnd = -1;
2427
+ if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1)
2428
+ break;
2429
+ parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
2430
+ }
2431
+ return chunk.slice(searchIndex);
2432
+ }
2433
+ function parseLine(chunk, start2, end) {
2434
+ if (start2 === end) {
2435
+ dispatchEvent();
2436
+ return;
2437
+ }
2438
+ const firstCharCode = chunk.charCodeAt(start2);
2439
+ if (isDataPrefix(chunk, start2, firstCharCode)) {
2440
+ const valueStart = chunk.charCodeAt(start2 + 5) === SPACE ? start2 + 6 : start2 + 5, value2 = chunk.slice(valueStart, end);
2441
+ data = dataLines === 0 ? value2 : `${data}
2442
+ ${value2}`, dataLines++;
2443
+ return;
2444
+ }
2445
+ if (isEventPrefix(chunk, start2, firstCharCode)) {
2446
+ eventType = chunk.slice(chunk.charCodeAt(start2 + 6) === SPACE ? start2 + 7 : start2 + 6, end) || void 0;
2447
+ return;
2448
+ }
2449
+ if (firstCharCode === 105 && chunk.charCodeAt(start2 + 1) === 100 && chunk.charCodeAt(start2 + 2) === 58) {
2450
+ const value2 = chunk.slice(chunk.charCodeAt(start2 + 3) === SPACE ? start2 + 4 : start2 + 3, end);
2451
+ id = value2.includes("\0") ? void 0 : value2;
2452
+ return;
2453
+ }
2454
+ if (firstCharCode === 58) {
2455
+ if (onComment) {
2456
+ const line2 = chunk.slice(start2, end);
2457
+ onComment(line2.slice(chunk.charCodeAt(start2 + 1) === SPACE ? 2 : 1));
2458
+ }
2459
+ return;
2460
+ }
2461
+ const line = chunk.slice(start2, end), fieldSeparatorIndex = line.indexOf(":");
2462
+ if (fieldSeparatorIndex === -1) {
2463
+ processField(line, "", line);
2464
+ return;
2465
+ }
2466
+ const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
2467
+ processField(field, value, line);
2468
+ }
2469
+ function processField(field, value, line) {
2470
+ switch (field) {
2471
+ case "event":
2472
+ eventType = value || void 0;
2473
+ break;
2474
+ case "data":
2475
+ data = dataLines === 0 ? value : `${data}
2476
+ ${value}`, dataLines++;
2477
+ break;
2478
+ case "id":
2479
+ id = value.includes("\0") ? void 0 : value;
2480
+ break;
2481
+ case "retry":
2482
+ /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
2483
+ new ParseError(`Invalid \`retry\` value: "${value}"`, {
2484
+ type: "invalid-retry",
2485
+ value,
2486
+ line
2487
+ })
2488
+ );
2489
+ break;
2490
+ default:
2491
+ onError(
2492
+ new ParseError(
2493
+ `Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
2494
+ { type: "unknown-field", field, value, line }
2495
+ )
2496
+ );
2497
+ break;
2498
+ }
2499
+ }
2500
+ function dispatchEvent() {
2501
+ dataLines > 0 && onEvent({
2502
+ id,
2503
+ event: eventType,
2504
+ data
2505
+ }), id = void 0, data = "", dataLines = 0, eventType = void 0;
2506
+ }
2507
+ function reset(options = {}) {
2508
+ if (options.consume && pendingFragments.length > 0) {
2509
+ const incompleteLine = pendingFragments.join("");
2510
+ parseLine(incompleteLine, 0, incompleteLine.length);
2511
+ }
2512
+ isFirstChunk = true, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0, pendingFragmentsLength = 0, terminated = false;
2513
+ }
2514
+ return { feed, reset };
2515
+ }
2516
+ function isDataPrefix(chunk, i, firstCharCode) {
2517
+ return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
2518
+ }
2519
+ function isEventPrefix(chunk, i, firstCharCode) {
2520
+ return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
2521
+ }
2522
+
2523
+ // src/realtime/sse.ts
2524
+ var STREAM_PATH = "/api/notifications/stream";
2525
+ var SseTransport = class {
2526
+ name = "sse";
2527
+ #idleTimeout;
2528
+ /** Идентификатор последнего события — отправляется при переподключении. */
2529
+ #lastEventId;
2530
+ constructor(options = {}) {
2531
+ this.#idleTimeout = options.idleTimeout ?? 9e4;
2532
+ }
2533
+ async connect(context) {
2534
+ const token = await context.getToken();
2535
+ if (!token) throw new UnauthorizedStreamError();
2536
+ const headers = new Headers({
2537
+ Accept: "text/event-stream",
2538
+ Authorization: `Bearer ${token}`,
2539
+ "Cache-Control": "no-cache"
2540
+ });
2541
+ if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
2542
+ const response = await context.fetch(joinUrl(context.baseUrl, STREAM_PATH), {
2543
+ method: "GET",
2544
+ headers,
2545
+ signal: context.signal
2546
+ });
2547
+ if (response.status === 401) throw new UnauthorizedStreamError();
2548
+ if (!response.ok) throw new Error(`\u041F\u043E\u0442\u043E\u043A \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
2549
+ if (!response.body) throw new Error("\u041E\u0442\u0432\u0435\u0442 \u043F\u043E\u0442\u043E\u043A\u0430 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u043F\u0443\u0441\u0442");
2550
+ context.onOpen();
2551
+ await this.#read(response.body, context);
2552
+ }
2553
+ async #read(body, context) {
2554
+ const reader = body.getReader();
2555
+ const decoder = new TextDecoder();
2556
+ const parser = createParser({
2557
+ onEvent: (message) => {
2558
+ if (message.id) this.#lastEventId = message.id;
2559
+ let data;
2560
+ try {
2561
+ data = JSON.parse(message.data);
2562
+ } catch (error) {
2563
+ context.onParseError(error, message.data);
2564
+ return;
2565
+ }
2566
+ const name = message.event ?? (typeof data === "object" && data !== null && "type" in data ? String(data.type) : "message");
2567
+ context.onEvent({ name, data });
2568
+ }
2569
+ });
2570
+ let idleTimer;
2571
+ const armIdleTimer = () => {
2572
+ if (this.#idleTimeout <= 0) return;
2573
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
2574
+ idleTimer = setTimeout(() => {
2575
+ void reader.cancel(new Error("\u041F\u043E\u0442\u043E\u043A \u043C\u043E\u043B\u0447\u0438\u0442 \u0434\u043E\u043B\u044C\u0448\u0435 \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0433\u043E")).catch(() => {
2576
+ });
2577
+ }, this.#idleTimeout);
2578
+ };
2579
+ armIdleTimer();
2580
+ try {
2581
+ for (; ; ) {
2582
+ const { done, value } = await reader.read();
2583
+ if (done) break;
2584
+ armIdleTimer();
2585
+ parser.feed(decoder.decode(value, { stream: true }));
2586
+ }
2587
+ } finally {
2588
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
2589
+ reader.releaseLock?.();
2590
+ }
2591
+ }
2592
+ };
2593
+
2594
+ // src/realtime/stream.ts
2595
+ var ItdRealtime = class {
2596
+ #deps;
2597
+ #options;
2598
+ #emitter = new Emitter();
2599
+ #transport;
2600
+ #maxAttempts;
2601
+ #controller;
2602
+ #status = RealtimeStatus.Disconnected;
2603
+ #attempt = 0;
2604
+ #timer;
2605
+ #detachEnvironment;
2606
+ constructor(deps, options = {}) {
2607
+ this.#deps = deps;
2608
+ this.#options = options;
2609
+ this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
2610
+ this.#transport = this.#createTransport();
2611
+ }
2612
+ /** Текущее состояние соединения. */
2613
+ get status() {
2614
+ return this.#status;
2615
+ }
2616
+ /** Какой транспорт используется: `sse` или `poll`. */
2617
+ get transport() {
2618
+ return this.#transport.name;
2619
+ }
2620
+ /** Подписывается на событие потока. @returns функция отписки */
2621
+ on(event, listener) {
2622
+ return this.#emitter.on(event, listener);
2623
+ }
2624
+ /** Подписывается на одно срабатывание. */
2625
+ once(event, listener) {
2626
+ return this.#emitter.once(event, listener);
2627
+ }
2628
+ /**
2629
+ * Поднимает соединение.
2630
+ *
2631
+ * Повторный вызов при уже живом соединении ничего не делает — это защита от двойного
2632
+ * подключения при перерисовке интерфейса.
2633
+ *
2634
+ * Возвращает управление сразу после запуска: соединение живёт в фоне.
2635
+ */
2636
+ async connect() {
2637
+ if (this.#controller) return;
2638
+ this.#attachEnvironmentListeners();
2639
+ if (this.#options.syncCount !== false) {
2640
+ try {
2641
+ this.#emitter.emit("unreadCount", await this.#deps.fetchUnreadCount());
2642
+ } catch (error) {
2643
+ 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);
2644
+ }
2645
+ }
2646
+ this.#run();
2647
+ }
2648
+ /** Закрывает соединение и отменяет запланированные попытки. */
2649
+ disconnect() {
2650
+ if (this.#timer !== void 0) {
2651
+ clearTimeout(this.#timer);
2652
+ this.#timer = void 0;
2653
+ }
2654
+ this.#detachEnvironment?.();
2655
+ this.#detachEnvironment = void 0;
2656
+ this.#controller?.abort();
2657
+ this.#controller = void 0;
2658
+ this.#attempt = 0;
2659
+ this.#setStatus(RealtimeStatus.Disconnected);
2660
+ }
2661
+ /** Снимает все подписки. Соединение при этом не закрывается. */
2662
+ removeAllListeners() {
2663
+ this.#emitter.removeAllListeners();
2664
+ }
2665
+ #createTransport() {
2666
+ const kind = this.#options.transport ?? "auto";
2667
+ if (typeof kind === "object") return kind;
2668
+ if (kind === "poll" || kind === "auto" && !supportsStreamingBody()) {
2669
+ return new PollTransport({
2670
+ ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
2671
+ });
2672
+ }
2673
+ return new SseTransport({
2674
+ ...this.#options.idleTimeout !== void 0 ? { idleTimeout: this.#options.idleTimeout } : {}
2675
+ });
2676
+ }
2677
+ /** Запускает попытку подключения; повторы планирует сам. */
2678
+ #run() {
2679
+ const controller = new AbortController();
2680
+ this.#controller = controller;
2681
+ this.#setStatus(RealtimeStatus.Connecting);
2682
+ void this.#transport.connect({
2683
+ baseUrl: this.#deps.baseUrl,
2684
+ fetch: this.#deps.fetch,
2685
+ getToken: this.#deps.getToken,
2686
+ signal: controller.signal,
2687
+ onOpen: () => {
2688
+ this.#attempt = 0;
2689
+ this.#setStatus(RealtimeStatus.Connected);
2690
+ },
2691
+ onEvent: (event) => this.#handleEvent(event.name, event.data),
2692
+ onParseError: (error, raw) => this.#emitter.emit("parseError", { error, raw })
2693
+ }).then(
2694
+ () => {
2695
+ if (!controller.signal.aborted) {
2696
+ this.#handleFailure(new Error("\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0441 \u043F\u043E\u0442\u043E\u043A\u043E\u043C \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0437\u0430\u043A\u0440\u044B\u0442\u043E"));
2697
+ }
2698
+ },
2699
+ (error) => {
2700
+ if (controller.signal.aborted) return;
2701
+ this.#handleFailure(error);
2702
+ }
2703
+ );
2704
+ }
2705
+ #handleEvent(name, data) {
2706
+ this.#emitter.emit("message", { name, data });
2707
+ if (name === "connected") {
2708
+ const userId = typeof data === "object" && data !== null && "userId" in data ? String(data.userId) : void 0;
2709
+ this.#emitter.emit("ready", { userId });
2710
+ return;
2711
+ }
2712
+ if (name === "notification") {
2713
+ const event = readNotificationEvent(data);
2714
+ this.#emitter.emit("notification", event);
2715
+ if (event.unreadCount !== void 0) this.#emitter.emit("unreadCount", event.unreadCount);
2716
+ return;
2717
+ }
2718
+ if (name === "unread_count") {
2719
+ const count = readUnreadCountEvent(data);
2720
+ if (count !== void 0) this.#emitter.emit("unreadCount", count);
2721
+ }
2722
+ }
2723
+ #handleFailure(error) {
2724
+ this.#controller = void 0;
2725
+ if (error instanceof UnauthorizedStreamError) {
2726
+ void this.#refreshAndReconnect(error);
2727
+ return;
2728
+ }
2729
+ this.#setStatus(RealtimeStatus.Error);
2730
+ this.#scheduleReconnect(error);
2731
+ }
2732
+ /** Обновляет токен и переподключается; при неудаче прекращает попытки. */
2733
+ async #refreshAndReconnect(error) {
2734
+ this.#setStatus(RealtimeStatus.Error);
2735
+ const refreshed = await this.#deps.refresh().catch(() => false);
2736
+ if (!refreshed) {
2737
+ this.#emitter.emit("error", { error, willReconnect: false });
2738
+ this.#emitter.emit("giveup", void 0);
2739
+ return;
2740
+ }
2741
+ this.#scheduleReconnect(error);
2742
+ }
2743
+ #scheduleReconnect(error) {
2744
+ if (this.#attempt >= this.#maxAttempts) {
2745
+ this.#emitter.emit("error", { error, willReconnect: false });
2746
+ this.#emitter.emit("giveup", void 0);
2747
+ return;
2748
+ }
2749
+ const delay = reconnectDelay(this.#attempt, this.#options);
2750
+ this.#attempt += 1;
2751
+ this.#emitter.emit("error", { error, willReconnect: true });
2752
+ this.#emitter.emit("reconnect", { attempt: this.#attempt, delay });
2753
+ this.#timer = setTimeout(() => {
2754
+ this.#timer = void 0;
2755
+ this.#run();
2756
+ }, delay);
2757
+ }
2758
+ /**
2759
+ * Подписывается на события среды.
2760
+ *
2761
+ * Возврат вкладки из фона и восстановление сети — самые частые причины «мёртвого»
2762
+ * соединения. У сайта итд.com такой обработки нет.
2763
+ */
2764
+ #attachEnvironmentListeners() {
2765
+ if (this.#detachEnvironment) return;
2766
+ const target = globalThis;
2767
+ if (typeof target.addEventListener !== "function") return;
2768
+ const wake = () => {
2769
+ if (this.#controller || this.#timer !== void 0) return;
2770
+ if (this.#status === RealtimeStatus.Disconnected) return;
2771
+ this.#attempt = 0;
2772
+ this.#run();
2773
+ };
2774
+ const onVisibility = () => {
2775
+ if (target.document?.visibilityState === "visible") wake();
2776
+ };
2777
+ const listeners = [];
2778
+ if (this.#options.reconnectOnVisible !== false && target.document) {
2779
+ listeners.push(["visibilitychange", onVisibility]);
2780
+ }
2781
+ if (this.#options.reconnectOnOnline !== false) {
2782
+ listeners.push(["online", wake]);
2783
+ }
2784
+ for (const [type, listener] of listeners) target.addEventListener(type, listener);
2785
+ this.#detachEnvironment = () => {
2786
+ for (const [type, listener] of listeners) target.removeEventListener?.(type, listener);
2787
+ };
2788
+ }
2789
+ #setStatus(status) {
2790
+ if (this.#status === status) return;
2791
+ this.#status = status;
2792
+ this.#emitter.emit("status", status);
2793
+ }
2794
+ };
2795
+
2796
+ // src/core/pagination.ts
2797
+ function readItems(body, fields) {
2798
+ if (Array.isArray(body)) return body;
2799
+ for (const field of fields) {
2800
+ const items = pickArray(body, field);
2801
+ if (items.length > 0) return items;
2802
+ }
2803
+ return fields.length > 0 ? pickArray(body, fields[0]) : [];
2804
+ }
2805
+ function readCursor(body) {
2806
+ const pagination = pickObject(body, "pagination");
2807
+ const meta = pickObject(body, "meta");
2808
+ const metaCursor = pickObject(meta, "cursor");
2809
+ return pickString(pagination, "nextCursor") ?? pickString(body, "nextCursor") ?? pickString(body, "cursor") ?? pickString(metaCursor, "next") ?? null;
2810
+ }
2811
+ function readCursorPage(body, ...fields) {
2812
+ const pagination = pickObject(body, "pagination");
2813
+ const items = readItems(body, fields);
2814
+ const nextCursor = readCursor(body);
2815
+ return {
2816
+ items,
2817
+ // Если признака продолжения нет, ориентируемся на наличие курсора.
2818
+ hasMore: pickBoolean(pagination, "hasMore", pickBoolean(body, "hasMore", nextCursor !== null)),
2819
+ nextCursor,
2820
+ limit: pickNumber(pagination, "limit", 0) || void 0,
2821
+ raw: body
2822
+ };
2823
+ }
2824
+ function readFlatCursorPage(body, ...fields) {
2825
+ const items = readItems(body, fields);
2826
+ const nextCursor = readCursor(body);
2827
+ const total = pickNumber(body, "total", -1);
2828
+ return {
2829
+ items,
2830
+ hasMore: pickBoolean(body, "hasMore", nextCursor !== null),
2831
+ nextCursor,
2832
+ ...total >= 0 ? { total } : {},
2833
+ raw: body
2834
+ };
2835
+ }
2836
+ function readPagedPage(body, ...fields) {
2837
+ const pagination = pickObject(body, "pagination");
2838
+ const items = readItems(body, fields);
2839
+ const nextCursor = readCursor(body);
2840
+ return {
2841
+ items,
2842
+ hasMore: pickBoolean(pagination, "hasMore", pickBoolean(body, "hasMore", nextCursor !== null)),
2843
+ page: pickNumber(pagination, "page", 1),
2844
+ limit: pickNumber(pagination, "limit", 0) || void 0,
2845
+ total: pickNumber(pagination, "total", 0),
2846
+ ...nextCursor !== null ? { nextCursor } : {},
2847
+ raw: body
2848
+ };
2849
+ }
2850
+ function readOffsetPage(body, field, offset) {
2851
+ const items = pickArray(body, field);
2852
+ return {
2853
+ items,
2854
+ hasMore: pickBoolean(body, "hasMore"),
2855
+ nextOffset: offset + items.length,
2856
+ raw: body
2857
+ };
2858
+ }
2859
+ var Paginator = class {
2860
+ #options;
2861
+ #maxPages;
2862
+ #state = {};
2863
+ #finished = false;
2864
+ #pagesLoaded = 0;
2865
+ constructor(options) {
2866
+ this.#options = options;
2867
+ this.#maxPages = options.maxPages ?? 1e3;
2868
+ }
2869
+ /**
2870
+ * Загружает следующую страницу.
2871
+ *
2872
+ * @returns страница либо `null`, если перебор закончен
2873
+ */
2874
+ async next() {
2875
+ if (this.#finished) return null;
2876
+ if (this.#options.signal?.aborted) return null;
2877
+ if (this.#pagesLoaded >= this.#maxPages) {
2878
+ this.#finished = true;
2879
+ return null;
2880
+ }
2881
+ const previous = this.#state;
2882
+ const page = await this.#options.load(previous);
2883
+ this.#pagesLoaded += 1;
2884
+ this.#state = this.#advance(previous, page);
2885
+ return page;
2886
+ }
2887
+ /**
2888
+ * Перебирает страницы целиком.
2889
+ *
2890
+ * Полезно, когда нужны сведения о самой странице — например `total`.
2891
+ */
2892
+ async *pages() {
2893
+ for (; ; ) {
2894
+ const page = await this.next();
2895
+ if (!page) return;
2896
+ yield page;
2897
+ }
2898
+ }
2899
+ /** Перебирает элементы всех страниц подряд. */
2900
+ async *[Symbol.asyncIterator]() {
2901
+ for await (const page of this.pages()) {
2902
+ for (const item of page.items) {
2903
+ if (this.#options.signal?.aborted) return;
2904
+ yield item;
2905
+ }
2906
+ }
2907
+ }
2908
+ /**
2909
+ * Собирает элементы в массив.
2910
+ *
2911
+ * @param max сколько элементов достаточно; без него перебираются все страницы
2912
+ */
2913
+ async collect(max) {
2914
+ const result = [];
2915
+ for await (const item of this) {
2916
+ result.push(item);
2917
+ if (max !== void 0 && result.length >= max) break;
2918
+ }
2919
+ return result;
2920
+ }
2921
+ /**
2922
+ * Вычисляет позицию следующей страницы и решает, продолжать ли.
2923
+ *
2924
+ * Здесь же стоят предохранители: пустая страница при `hasMore`, неизменившийся курсор
2925
+ * и отсутствие курсора останавливают перебор. Без них ошибка на сервере превратилась бы
2926
+ * в бесконечный цикл запросов.
2927
+ */
2928
+ #advance(previous, page) {
2929
+ if (!page.hasMore || page.items.length === 0) {
2930
+ this.#finished = true;
2931
+ return previous;
2932
+ }
2933
+ if (this.#options.mode === "cursor") {
2934
+ const cursor = page.nextCursor ?? void 0;
2935
+ if (!cursor || cursor === previous.cursor) {
2936
+ this.#finished = true;
2937
+ return previous;
2938
+ }
2939
+ return { cursor };
2940
+ }
2941
+ if (this.#options.mode === "page") {
2942
+ return { page: (previous.page ?? 1) + 1 };
2943
+ }
2944
+ return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
2945
+ }
2946
+ };
2947
+
2948
+ // src/resources/base.ts
2949
+ var BaseResource = class {
2950
+ /** @internal */
2951
+ http;
2952
+ constructor(http) {
2953
+ this.http = http;
2954
+ }
2955
+ /** Переносит общие поля опций запроса в параметры транспорта. */
2956
+ requestOptions(options) {
2957
+ if (!options) return {};
2958
+ return {
2959
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
2960
+ ...options.timeout !== void 0 ? { timeout: options.timeout } : {},
2961
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
2962
+ ...options.retry !== void 0 ? { retry: options.retry } : {}
2963
+ };
2964
+ }
2965
+ /**
2966
+ * Собирает перебор страниц.
2967
+ *
2968
+ * @param mode схема пагинации эндпоинта
2969
+ * @param load загружает одну страницу для указанной позиции
2970
+ */
2971
+ paginate(mode, load, options) {
2972
+ return new Paginator({
2973
+ mode,
2974
+ load,
2975
+ ...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
2976
+ ...options?.signal !== void 0 ? { signal: options.signal } : {}
2977
+ });
2978
+ }
2979
+ };
2980
+ function withPageState(query, state) {
2981
+ return {
2982
+ ...query,
2983
+ ...state.cursor !== void 0 ? { cursor: state.cursor } : {},
2984
+ ...state.page !== void 0 ? { page: state.page } : {},
2985
+ ...state.offset !== void 0 ? { offset: state.offset } : {}
2986
+ };
2987
+ }
2988
+
2989
+ // src/resources/auth.ts
2990
+ var AuthResource = class extends BaseResource {
2991
+ #auth;
2992
+ constructor(http, deps) {
2993
+ super(http);
2994
+ this.#auth = deps.auth;
2995
+ }
2996
+ /**
2997
+ * Регистрирует аккаунт и запускает подтверждение по коду.
2998
+ *
2999
+ * @returns `flowToken`, который нужно передать в {@link verifyOtp}
3000
+ */
3001
+ async signUp(credentials, options = {}) {
3002
+ const body = await this.http.request({
3003
+ method: "POST",
3004
+ path: "/api/v1/auth/sign-up",
3005
+ body: credentials,
3006
+ skipAuth: true,
3007
+ skipAuthRefresh: true,
3008
+ ...this.requestOptions(options)
3009
+ });
3010
+ const flowToken = pickString(body, "flowToken");
3011
+ if (!flowToken) {
3012
+ throw new ItdConfigError("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u043F\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438");
3013
+ }
3014
+ return flowToken;
3015
+ }
3016
+ /**
3017
+ * Выполняет вход.
3018
+ *
3019
+ * Если сервер потребовал код подтверждения, вернётся `status: 'otp_required'` —
3020
+ * тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
3021
+ *
3022
+ * При успешном входе токен сохраняется в клиенте автоматически.
3023
+ */
3024
+ async signIn(credentials, options = {}) {
3025
+ const body = await this.http.request({
3026
+ method: "POST",
3027
+ path: "/api/v1/auth/sign-in",
3028
+ body: credentials,
3029
+ skipAuth: true,
3030
+ skipAuthRefresh: true,
3031
+ ...this.requestOptions(options)
3032
+ });
3033
+ const accessToken = pickString(body, "accessToken");
3034
+ if (accessToken) {
3035
+ await this.#auth.setAccessToken(accessToken);
3036
+ return { status: "authenticated", accessToken };
3037
+ }
3038
+ return { status: "otp_required", flowToken: pickString(body, "flowToken") };
3039
+ }
3040
+ /**
3041
+ * Подтверждает вход кодом из письма.
3042
+ *
3043
+ * Полученный токен сохраняется в клиенте автоматически.
3044
+ */
3045
+ async verifyOtp(input, options = {}) {
3046
+ const body = await this.http.request({
3047
+ method: "POST",
3048
+ path: "/api/v1/auth/verify-otp",
3049
+ body: input,
3050
+ skipAuth: true,
3051
+ skipAuthRefresh: true,
3052
+ ...this.requestOptions(options)
3053
+ });
3054
+ const accessToken = pickString(body, "accessToken");
3055
+ if (!accessToken) {
3056
+ throw new ItdConfigError("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B accessToken \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u043A\u043E\u0434\u0430");
3057
+ }
3058
+ await this.#auth.setAccessToken(accessToken);
3059
+ return accessToken;
3060
+ }
3061
+ /** Отправляет код подтверждения повторно. */
3062
+ resendOtp(input, options = {}) {
3063
+ return this.http.request({
3064
+ method: "POST",
3065
+ path: "/api/v1/auth/resend-otp",
3066
+ body: input,
3067
+ skipAuth: true,
3068
+ skipAuthRefresh: true,
3069
+ ...this.requestOptions(options)
3070
+ });
3071
+ }
3072
+ /**
3073
+ * Полный вход с подтверждением по коду.
3074
+ *
3075
+ * Удобно для скриптов и ботов: код запрашивается функцией `getOtp`, а всё остальное
3076
+ * библиотека делает сама.
3077
+ *
3078
+ * @example
3079
+ * ```ts
3080
+ * import { createInterface } from 'node:readline/promises';
3081
+ *
3082
+ * const rl = createInterface({ input: process.stdin, output: process.stdout });
3083
+ *
3084
+ * const token = await itd.auth.signInWithOtp({
3085
+ * email, password,
3086
+ * getOtp: () => rl.question('Код из письма: '),
3087
+ * });
3088
+ * ```
3089
+ */
3090
+ async signInWithOtp(input, options = {}) {
3091
+ const { getOtp, ...credentials } = input;
3092
+ const result = await this.signIn(credentials, options);
3093
+ if (result.status === "authenticated") return result.accessToken;
3094
+ if (!result.flowToken) {
3095
+ throw new ItdConfigError(
3096
+ "\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"
3097
+ );
3098
+ }
3099
+ const otp = await getOtp();
3100
+ return this.verifyOtp({ ...credentials, otp, flowToken: result.flowToken }, options);
3101
+ }
3102
+ /**
3103
+ * Обновляет токен доступа.
3104
+ *
3105
+ * Параллельные вызовы объединяются в один сетевой запрос. При включённом `autoRefresh`
3106
+ * вызывать вручную обычно не нужно.
3107
+ */
3108
+ refresh() {
3109
+ return this.#auth.refresh();
3110
+ }
3111
+ /**
3112
+ * Есть ли признак живой сессии обновления.
3113
+ *
3114
+ * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном. Позволяет
3115
+ * не дёргать API у неавторизованного пользователя. В браузере всегда `true`:
3116
+ * cookie ведёт сама среда, и прочитать её из JS нельзя.
3117
+ */
3118
+ hasRefreshSession() {
3119
+ return this.#auth.hasRefreshSession();
3120
+ }
3121
+ /** Завершает текущую сессию на сервере и очищает локальную. */
3122
+ async logout(options = {}) {
3123
+ await this.http.request({
3124
+ method: "POST",
3125
+ path: "/api/v1/auth/logout",
3126
+ skipAuthRefresh: true,
3127
+ ...this.requestOptions(options)
3128
+ });
3129
+ await this.#auth.clear();
3130
+ }
3131
+ /** Завершает все сессии пользователя и очищает локальную. */
3132
+ async logoutAll(options = {}) {
3133
+ await this.http.request({
3134
+ method: "POST",
3135
+ path: "/api/v1/auth/logout-all",
3136
+ skipAuthRefresh: true,
3137
+ ...this.requestOptions(options)
3138
+ });
3139
+ await this.#auth.clear();
3140
+ }
3141
+ /** Забывает сессию локально, не обращаясь к серверу. */
3142
+ signOut() {
3143
+ return this.#auth.clear();
3144
+ }
3145
+ /** Запрашивает письмо для сброса пароля. */
3146
+ forgotPassword(email, options = {}) {
3147
+ return this.http.request({
3148
+ method: "POST",
3149
+ path: "/api/v1/auth/forgot-password",
3150
+ body: { email },
3151
+ skipAuth: true,
3152
+ skipAuthRefresh: true,
3153
+ ...this.requestOptions(options)
3154
+ });
3155
+ }
3156
+ /** Устанавливает новый пароль по токену из письма. */
3157
+ resetPassword(input, options = {}) {
3158
+ return this.http.request({
3159
+ method: "POST",
3160
+ path: "/api/v1/auth/reset-password",
3161
+ body: input,
3162
+ skipAuth: true,
3163
+ skipAuthRefresh: true,
3164
+ ...this.requestOptions(options)
3165
+ });
3166
+ }
3167
+ /** Меняет пароль. Требует действующей сессии обновления. */
3168
+ changePassword(input, options = {}) {
3169
+ return this.http.request({
3170
+ method: "POST",
3171
+ path: "/api/v1/auth/change-password",
3172
+ body: input,
3173
+ ...this.requestOptions(options)
3174
+ });
3175
+ }
3176
+ /**
3177
+ * Возвращает адрес для входа через внешнего провайдера.
3178
+ *
3179
+ * Сам переход выполняет приложение: в браузере — редиректом, в приложении — открытием
3180
+ * системного браузера.
3181
+ *
3182
+ * @example
3183
+ * ```ts
3184
+ * window.location.href = itd.auth.oauthUrl('yandex');
3185
+ * ```
3186
+ */
3187
+ oauthUrl(provider) {
3188
+ return joinUrl(this.http.baseUrl, `/api/v1/auth/login/${provider}`);
3189
+ }
3190
+ /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
3191
+ async sessions(options = {}) {
3192
+ const body = await this.http.request({
3193
+ method: "GET",
3194
+ path: "/api/v1/auth/sessions",
3195
+ ...this.requestOptions(options)
3196
+ });
3197
+ return pickArray(body, "sessions");
3198
+ }
3199
+ /** Завершает указанную сессию. */
3200
+ revokeSession(sessionId, options = {}) {
3201
+ return this.http.request({
3202
+ method: "DELETE",
3203
+ path: `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`,
3204
+ ...this.requestOptions(options)
3205
+ });
3206
+ }
3207
+ /** Завершает все сессии, кроме текущей. */
3208
+ revokeOtherSessions(options = {}) {
3209
+ return this.http.request({
3210
+ method: "DELETE",
3211
+ path: "/api/v1/auth/sessions",
3212
+ ...this.requestOptions(options)
3213
+ });
3214
+ }
3215
+ };
3216
+
3217
+ // src/resources/comments.ts
3218
+ var CommentsResource = class extends BaseResource {
3219
+ #uploadFiles;
3220
+ constructor(http, deps) {
3221
+ super(http);
3222
+ this.#uploadFiles = deps.uploadFiles;
3223
+ }
3224
+ /**
3225
+ * Загружает страницу ответов на комментарий.
3226
+ *
3227
+ * Здесь пагинация **постраничная**, в отличие от комментариев к посту, где курсорная.
3228
+ */
3229
+ async replies(commentId, params = {}) {
3230
+ const body = await this.http.request({
3231
+ method: "GET",
3232
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
3233
+ query: { limit: params.limit, page: params.page },
3234
+ ...this.requestOptions(params)
3235
+ });
3236
+ return readPagedPage(body, "replies");
3237
+ }
3238
+ /** Перебирает ответы на комментарий. */
3239
+ iterateReplies(commentId, params = {}) {
3240
+ const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
3241
+ return this.paginate(
3242
+ "page",
3243
+ async (state) => {
3244
+ const body = await this.http.request({
3245
+ method: "GET",
3246
+ path,
3247
+ query: withPageState({ limit: params.limit }, state),
3248
+ ...this.requestOptions(params)
3249
+ });
3250
+ return readPagedPage(body, "replies");
3251
+ },
3252
+ params
3253
+ );
3254
+ }
3255
+ /**
3256
+ * Отвечает на комментарий.
3257
+ *
3258
+ * @example
3259
+ * ```ts
3260
+ * await itd.comments.reply(commentId, 'согласен');
3261
+ * await itd.comments.reply(commentId, (c) => c.content('и вот почему').replyTo(userId));
3262
+ * ```
3263
+ */
3264
+ async reply(commentId, input, options = {}) {
3265
+ const data = resolveComment(typeof input === "string" ? { content: input } : input, true);
3266
+ const existing = data.attachmentIds ?? [];
3267
+ const files = data.files ?? [];
3268
+ const attachmentIds = files.length > 0 ? [...existing, ...await this.#uploadFiles(files, options)] : existing;
3269
+ return this.http.request({
3270
+ method: "POST",
3271
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
3272
+ body: {
3273
+ content: data.content ?? "",
3274
+ attachmentIds,
3275
+ ...data.replyToUserId ? { replyToUserId: data.replyToUserId } : {}
3276
+ },
3277
+ ...this.requestOptions(options)
3278
+ });
3279
+ }
3280
+ /** Редактирует текст комментария. */
3281
+ update(commentId, content, options = {}) {
3282
+ return this.http.request({
3283
+ method: "PATCH",
3284
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}`,
3285
+ body: { content },
3286
+ ...this.requestOptions(options)
3287
+ });
3288
+ }
3289
+ /** Удаляет комментарий. Восстановить его можно через {@link restore}. */
3290
+ remove(commentId, options = {}) {
3291
+ return this.http.request({
3292
+ method: "DELETE",
3293
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}`,
3294
+ ...this.requestOptions(options)
3295
+ });
3296
+ }
3297
+ /** Восстанавливает удалённый комментарий. */
3298
+ restore(commentId, options = {}) {
3299
+ return this.http.request({
3300
+ method: "POST",
3301
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/restore`,
3302
+ ...this.requestOptions(options)
3303
+ });
3304
+ }
3305
+ /** Ставит реакцию на комментарий. */
3306
+ like(commentId, options = {}) {
3307
+ return this.http.request({
3308
+ method: "POST",
3309
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
3310
+ ...this.requestOptions(options)
3311
+ });
3312
+ }
3313
+ /** Убирает реакцию с комментария. */
3314
+ unlike(commentId, options = {}) {
3315
+ return this.http.request({
3316
+ method: "DELETE",
3317
+ path: `/api/comments/${encodePathSegment(commentId, "commentId")}/like`,
3318
+ ...this.requestOptions(options)
3319
+ });
3320
+ }
3321
+ };
3322
+
3323
+ // src/core/mime.ts
3324
+ var IMAGE_MIME_TYPES = Object.freeze([
3325
+ "image/jpeg",
3326
+ "image/png",
3327
+ "image/gif",
3328
+ "image/webp",
3329
+ "image/avif",
3330
+ "image/heic",
3331
+ "image/heif"
3332
+ ]);
3333
+ var VIDEO_MIME_TYPES = Object.freeze(["video/mp4", "video/webm", "video/quicktime"]);
3334
+ var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
3335
+ var ALLOWED_MIME_TYPES = Object.freeze([
3336
+ ...IMAGE_MIME_TYPES,
3337
+ ...VIDEO_MIME_TYPES,
3338
+ ...AUDIO_MIME_TYPES
3339
+ ]);
3340
+ var EXTENSION_TO_MIME = Object.freeze({
3341
+ jpg: "image/jpeg",
3342
+ jpeg: "image/jpeg",
3343
+ jfif: "image/jpeg",
3344
+ png: "image/png",
3345
+ gif: "image/gif",
3346
+ webp: "image/webp",
3347
+ avif: "image/avif",
3348
+ heic: "image/heic",
3349
+ heif: "image/heif",
3350
+ mp4: "video/mp4",
3351
+ m4v: "video/mp4",
3352
+ webm: "video/webm",
3353
+ mov: "video/quicktime",
3354
+ qt: "video/quicktime",
3355
+ ogg: "audio/ogg",
3356
+ oga: "audio/ogg",
3357
+ opus: "audio/ogg"
3358
+ });
3359
+ function mimeFromFilename(filename) {
3360
+ const dot = filename.lastIndexOf(".");
3361
+ if (dot < 0) return void 0;
3362
+ return EXTENSION_TO_MIME[filename.slice(dot + 1).toLowerCase()];
3363
+ }
3364
+ function isAllowedMime(mimeType) {
3365
+ return ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase());
3366
+ }
3367
+ function assertAllowedMime(mimeType, filename) {
3368
+ if (!mimeType) {
3369
+ throw new ItdConfigError(
3370
+ `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430${filename ? ` \xAB${filename}\xBB` : ""}. \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0435\u0433\u043E \u044F\u0432\u043D\u043E: { data, filename, contentType }.`
3371
+ );
3372
+ }
3373
+ if (!isAllowedMime(mimeType)) {
3374
+ throw new ItdConfigError(
3375
+ `\u0422\u0438\u043F \xAB${mimeType}\xBB \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u043E\u0439. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: ${ALLOWED_MIME_TYPES.join(", ")}.`
3376
+ );
3377
+ }
3378
+ }
3379
+
3380
+ // src/resources/files.ts
3381
+ var DEFAULT_UPLOAD_TIMEOUT = 3e5;
3382
+ var FilesResource = class extends BaseResource {
3383
+ #readFile;
3384
+ constructor(http, deps = {}) {
3385
+ super(http);
3386
+ this.#readFile = deps.readFile;
3387
+ }
3388
+ /**
3389
+ * Подключает чтение файлов с диска.
3390
+ *
3391
+ * Вызывается точкой входа `itd-api/node`; в основном бандле работы с файловой
3392
+ * системой нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
3393
+ */
3394
+ setFileReader(readFile) {
3395
+ this.#readFile = readFile;
3396
+ }
3397
+ /**
3398
+ * Загружает файл и возвращает его идентификатор.
3399
+ *
3400
+ * @remarks
3401
+ * Кроме типа сервер проверяет и само изображение: слишком маленькие картинки
3402
+ * он отклоняет сообщением «Не удалось проверить изображение». Точный порог
3403
+ * неизвестен, но 64×64 проходит.
3404
+ *
3405
+ * @example
3406
+ * ```ts
3407
+ * const file = await itd.files.upload(blob, { filename: 'photo.jpg' });
3408
+ * await itd.posts.create({ content: 'смотрите', attachmentIds: [file.id] });
3409
+ * ```
3410
+ */
3411
+ async upload(input, options = {}) {
3412
+ const prepared = await this.prepare(input, options);
3413
+ const form = new FormData();
3414
+ form.set("file", prepared.blob, prepared.filename);
3415
+ return this.http.request({
3416
+ method: "POST",
3417
+ path: "/api/files/upload",
3418
+ body: form,
3419
+ timeout: options.timeout ?? DEFAULT_UPLOAD_TIMEOUT,
3420
+ ...this.requestOptions(options)
3421
+ });
3422
+ }
3423
+ /**
3424
+ * Загружает несколько файлов, сохраняя порядок.
3425
+ *
3426
+ * Файлы отправляются последовательно: параллельная загрузка нескольких видео легко
3427
+ * упирается в ограничение частоты, а порядок вложений в посте важен.
3428
+ *
3429
+ * @returns идентификаторы вложений в том же порядке, что и входные файлы
3430
+ */
3431
+ async uploadMany(files, options = {}) {
3432
+ const ids = [];
3433
+ for (const file of files) {
3434
+ const uploaded = await this.upload(file, options);
3435
+ ids.push(uploaded.id);
3436
+ }
3437
+ return ids;
3438
+ }
3439
+ /**
3440
+ * Загружает сведения о файле.
3441
+ *
3442
+ * @remarks
3443
+ * Сервер отвечает `404` даже на только что загруженный файл, который ещё никуда
3444
+ * не прикреплён, — проверено на боевом API. Практической пользы у метода пока нет,
3445
+ * он оставлен для полноты.
3446
+ */
3447
+ get(fileId, options = {}) {
3448
+ return this.http.request({
3449
+ method: "GET",
3450
+ path: `/api/files/${encodePathSegment(fileId, "fileId")}`,
3451
+ ...this.requestOptions(options)
3452
+ });
3453
+ }
3454
+ /** Удаляет загруженный файл. */
3455
+ remove(fileId, options = {}) {
3456
+ return this.http.request({
3457
+ method: "DELETE",
3458
+ path: `/api/files/${encodePathSegment(fileId, "fileId")}`,
3459
+ ...this.requestOptions(options)
3460
+ });
3461
+ }
3462
+ /** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
3463
+ async prepare(input, options) {
3464
+ const { data, filename, contentType } = await this.#normalize(input, options);
3465
+ const type = contentType ?? ((data instanceof Blob ? data.type : void 0) || mimeFromFilename(filename));
3466
+ if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
3467
+ const blob = data instanceof Blob && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
3468
+ return { blob, filename };
3469
+ }
3470
+ async #normalize(input, options) {
3471
+ if (typeof input === "string") {
3472
+ if (!this.#readFile) {
3473
+ throw new ItdConfigError(
3474
+ `\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E \u043F\u0443\u0442\u0438 \xAB${input}\xBB \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 Node, Bun \u0438 Deno. \u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0451 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u043C 'itd-api/node' \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 Blob \u0438\u043B\u0438 File.`
3475
+ );
3476
+ }
3477
+ const file = await this.#readFile(input);
3478
+ return {
3479
+ data: file.data,
3480
+ filename: options.filename ?? file.filename,
3481
+ ...options.contentType ? { contentType: options.contentType } : {}
3482
+ };
3483
+ }
3484
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input instanceof Blob) {
3485
+ const fallbackName = input instanceof File ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
3486
+ return {
3487
+ data: input,
3488
+ filename: options.filename ?? fallbackName,
3489
+ ...options.contentType ? { contentType: options.contentType } : {}
3490
+ };
3491
+ }
3492
+ const contentType = input.contentType ?? options.contentType;
3493
+ return {
3494
+ data: input.data,
3495
+ filename: input.filename ?? options.filename ?? this.#nameFromMime(contentType),
3496
+ ...contentType ? { contentType } : {}
3497
+ };
3498
+ }
3499
+ /** Подбирает имя файла, когда его не передали: сервер ждёт непустое поле. */
3500
+ #nameFromMime(contentType) {
3501
+ const extension = contentType?.split("/")[1]?.split(";")[0];
3502
+ return extension ? `file.${extension}` : "file";
3503
+ }
3504
+ };
3505
+
3506
+ // src/resources/misc.ts
3507
+ var HashtagsResource = class extends BaseResource {
3508
+ /**
3509
+ * Ищет хэштеги.
3510
+ *
3511
+ * Без строки запроса возвращает общий список.
3512
+ */
3513
+ async search(query, params = {}) {
3514
+ const body = await this.http.request({
3515
+ method: "GET",
3516
+ path: "/api/hashtags",
3517
+ query: { q: query, limit: params.limit },
3518
+ ...this.requestOptions(params)
3519
+ });
3520
+ return pickArray(body, "hashtags");
3521
+ }
3522
+ /** Загружает трендовые хэштеги. */
3523
+ async trending(params = {}) {
3524
+ const body = await this.http.request({
3525
+ method: "GET",
3526
+ path: "/api/hashtags/trending",
3527
+ query: { limit: params.limit },
3528
+ ...this.requestOptions(params)
3529
+ });
3530
+ return pickArray(body, "hashtags");
3531
+ }
3532
+ /**
3533
+ * Загружает страницу постов по хэштегу.
3534
+ *
3535
+ * @param tag название без решётки; кодируется автоматически, поэтому кириллица
3536
+ * и пробелы допустимы
3537
+ */
3538
+ async posts(tag, params = {}) {
3539
+ const body = await this.http.request({
3540
+ method: "GET",
3541
+ path: `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`,
3542
+ query: { limit: params.limit, cursor: params.cursor },
3543
+ ...this.requestOptions(params)
3544
+ });
3545
+ return readCursorPage(body, "posts");
3546
+ }
3547
+ /** Перебирает посты по хэштегу. */
3548
+ iteratePosts(tag, params = {}) {
3549
+ const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
3550
+ return this.paginate(
3551
+ "cursor",
3552
+ async (state) => {
3553
+ const body = await this.http.request({
3554
+ method: "GET",
3555
+ path,
3556
+ query: withPageState({ limit: params.limit }, state),
3557
+ ...this.requestOptions(params)
3558
+ });
3559
+ return readCursorPage(body, "posts");
3560
+ },
3561
+ params
3562
+ );
3563
+ }
3564
+ };
3565
+ var SearchResource = class extends BaseResource {
3566
+ /**
3567
+ * Ищет пользователей и хэштеги одним запросом.
3568
+ *
3569
+ * @example
3570
+ * ```ts
3571
+ * const { users, hashtags } = await itd.search.all('арт');
3572
+ * ```
3573
+ */
3574
+ async all(query, options = {}) {
3575
+ const body = await this.http.request({
3576
+ method: "GET",
3577
+ path: "/api/search",
3578
+ query: { q: query },
3579
+ ...this.requestOptions(options)
3580
+ });
3581
+ return {
3582
+ users: pickArray(body, "users"),
3583
+ hashtags: pickArray(body, "hashtags")
3584
+ };
3585
+ }
3586
+ };
3587
+ var ReportsResource = class extends BaseResource {
3588
+ /**
3589
+ * Отправляет жалобу.
3590
+ *
3591
+ * Повторная жалоба на тот же объект отклоняется сервером с сообщением
3592
+ * «Вы уже отправляли жалобу на этот контент».
3593
+ *
3594
+ * @example
3595
+ * ```ts
3596
+ * await itd.reports.create(report.post(postId).reason('spam'));
3597
+ * await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
3598
+ * ```
3599
+ */
3600
+ create(input, options = {}) {
3601
+ const data = resolveReport(input);
3602
+ return this.http.request({
3603
+ method: "POST",
3604
+ path: "/api/reports",
3605
+ body: data,
3606
+ ...this.requestOptions(options)
3607
+ });
3608
+ }
3609
+ };
3610
+ var VerificationResource = class extends BaseResource {
3611
+ /** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
3612
+ status(options = {}) {
3613
+ return this.http.request({
3614
+ method: "GET",
3615
+ path: "/api/verification/status",
3616
+ ...this.requestOptions(options)
3617
+ });
3618
+ }
3619
+ /** Подаёт заявку на верификацию с видео. */
3620
+ submit(videoUrl, options = {}) {
3621
+ return this.http.request({
3622
+ method: "POST",
3623
+ path: "/api/verification/submit",
3624
+ body: { videoUrl },
3625
+ ...this.requestOptions(options)
3626
+ });
3627
+ }
3628
+ };
3629
+ var SubscriptionResource = class extends BaseResource {
3630
+ /** Загружает состояние подписки и её цену. */
3631
+ status(options = {}) {
3632
+ return this.http.request({
3633
+ method: "GET",
3634
+ // Завершающий слэш обязателен.
3635
+ path: "/api/v1/subscription/",
3636
+ ...this.requestOptions(options)
3637
+ });
3638
+ }
3639
+ /**
3640
+ * Запускает оплату подписки.
3641
+ *
3642
+ * Форма ответа в документации API не описана, поэтому тип результата не уточняется.
3643
+ */
3644
+ pay(options = {}) {
3645
+ return this.http.request({
3646
+ method: "POST",
3647
+ path: "/api/v1/subscription/pay",
3648
+ ...this.requestOptions(options)
3649
+ });
3650
+ }
3651
+ /** Включает или отключает автопродление. */
3652
+ setAutoRenewal(enabled, options = {}) {
3653
+ return this.http.request({
3654
+ method: "POST",
3655
+ path: "/api/v1/subscription/auto-renewal",
3656
+ body: { enabled },
3657
+ ...this.requestOptions(options)
3658
+ });
3659
+ }
3660
+ /** Запускает привязку карты. */
3661
+ bindCard(options = {}) {
3662
+ return this.http.request({
3663
+ method: "POST",
3664
+ path: "/api/v1/subscription/bind-card",
3665
+ ...this.requestOptions(options)
3666
+ });
3667
+ }
3668
+ /** Загружает список способов оплаты. Пустой массив, если карт нет. */
3669
+ async methods(options = {}) {
3670
+ const body = await this.http.request({
3671
+ method: "GET",
3672
+ path: "/api/v1/subscription/methods",
3673
+ ...this.requestOptions(options)
3674
+ });
3675
+ return Array.isArray(body) ? body : [];
3676
+ }
3677
+ /** Делает способ оплаты основным. */
3678
+ setDefaultMethod(methodId, options = {}) {
3679
+ return this.http.request({
3680
+ method: "POST",
3681
+ path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
3682
+ ...this.requestOptions(options)
3683
+ });
3684
+ }
3685
+ /** Удаляет способ оплаты. */
3686
+ removeMethod(methodId, options = {}) {
3687
+ return this.http.request({
3688
+ method: "DELETE",
3689
+ path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
3690
+ ...this.requestOptions(options)
3691
+ });
3692
+ }
3693
+ };
3694
+ var PlatformResource = class extends BaseResource {
3695
+ /** Загружает журнал изменений. */
3696
+ async changelog(options = {}) {
3697
+ const body = await this.http.request({
3698
+ method: "GET",
3699
+ path: "/api/platform/changelog",
3700
+ ...this.requestOptions(options)
3701
+ });
3702
+ return Array.isArray(body) ? body : [];
3703
+ }
3704
+ /** Загружает анонсы платформы. */
3705
+ async announcements(options = {}) {
3706
+ const body = await this.http.request({
3707
+ method: "GET",
3708
+ path: "/api/platform/announcements",
3709
+ ...this.requestOptions(options)
3710
+ });
3711
+ return pickArray(body, "announcements");
3712
+ }
3713
+ /** Загружает баннер текущего события — виджет «портал». */
3714
+ portal(options = {}) {
3715
+ return this.http.request({
3716
+ method: "GET",
3717
+ path: "/api/v1/portal",
3718
+ ...this.requestOptions(options)
3719
+ });
3720
+ }
3721
+ };
3722
+ var TelemetryResource = class extends BaseResource {
3723
+ /**
3724
+ * Отправляет время просмотра постов.
3725
+ *
3726
+ * @experimental Имена полей на проводе сжаты (`ai`, `v`, `s`), и их соответствие
3727
+ * смыслу **не проверено** на реальных запросах. Может измениться без предупреждения.
3728
+ */
3729
+ dwell(entries, options = {}) {
3730
+ return this.http.request({
3731
+ method: "POST",
3732
+ path: "/api/v1/i",
3733
+ body: {
3734
+ items: entries.map((entry) => ({
3735
+ ai: entry.postId,
3736
+ v: entry.duration,
3737
+ ...entry.vs ? { s: entry.vs } : {}
3738
+ }))
3739
+ },
3740
+ ...this.requestOptions(options)
3741
+ });
3742
+ }
3743
+ /**
3744
+ * Отправляет события взаимодействия с контентом.
3745
+ *
3746
+ * @experimental См. предупреждение у {@link TelemetryResource}.
3747
+ */
3748
+ interaction(entries, options = {}) {
3749
+ return this.http.request({
3750
+ method: "POST",
3751
+ path: "/api/v1/x",
3752
+ body: {
3753
+ items: entries.map((entry) => ({
3754
+ t: entry.type,
3755
+ ...entry.value !== void 0 ? { v: entry.value } : {},
3756
+ ...entry.postId ? { ai: entry.postId } : {},
3757
+ ...entry.attachmentId ? { mi: entry.attachmentId } : {},
3758
+ ...entry.vs ? { s: entry.vs } : {}
3759
+ }))
3760
+ },
3761
+ ...this.requestOptions(options)
3762
+ });
3763
+ }
3764
+ };
3765
+
3766
+ // src/resources/notifications.ts
3767
+ var READ_BATCH_SIZE = 20;
3768
+ function readSettings(body) {
3769
+ return {
3770
+ enabled: pickBoolean(body, "enabled", true),
3771
+ sound: pickBoolean(body, "sound", true),
3772
+ follows: pickBoolean(body, "follows", true),
3773
+ wallPosts: pickBoolean(body, "wallPosts", true),
3774
+ likes: pickBoolean(body, "likes", true),
3775
+ comments: pickBoolean(body, "comments", true),
3776
+ mentions: pickBoolean(body, "mentions", true)
3777
+ };
3778
+ }
3779
+ var NotificationsResource = class extends BaseResource {
3780
+ /**
3781
+ * Загружает страницу уведомлений.
3782
+ *
3783
+ * Пагинация здесь основана на смещении. Сайт итд.com оборачивает смещение в строку
3784
+ * и притворяется, что это курсор; библиотека отдаёт честное число.
3785
+ *
3786
+ * @example
3787
+ * ```ts
3788
+ * const page = await itd.notifications.list({ limit: 20 });
3789
+ * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
3790
+ * ```
3791
+ */
3792
+ async list(params = {}) {
3793
+ const offset = params.offset ?? 0;
3794
+ const body = await this.http.request({
3795
+ method: "GET",
3796
+ // Завершающий слэш обязателен: без него сервер отвечает ошибкой.
3797
+ path: "/api/notifications/",
3798
+ query: { limit: params.limit, offset },
3799
+ ...this.requestOptions(params)
3800
+ });
3801
+ const page = readOffsetPage(body, "notifications", offset);
3802
+ return { ...page, items: page.items.map(normalizeNotification) };
3803
+ }
3804
+ /**
3805
+ * Перебирает уведомления.
3806
+ *
3807
+ * @example
3808
+ * ```ts
3809
+ * for await (const notification of itd.notifications.iterate()) {
3810
+ * console.log(formatNotificationText(notification));
3811
+ * }
3812
+ * ```
3813
+ */
3814
+ iterate(params = {}) {
3815
+ return this.paginate(
3816
+ "offset",
3817
+ async (state) => {
3818
+ const offset = state.offset ?? params.offset ?? 0;
3819
+ const body = await this.http.request({
3820
+ method: "GET",
3821
+ path: "/api/notifications/",
3822
+ query: { limit: params.limit, offset },
3823
+ ...this.requestOptions(params)
3824
+ });
3825
+ const page = readOffsetPage(body, "notifications", offset);
3826
+ return { ...page, items: page.items.map(normalizeNotification) };
3827
+ },
3828
+ params
3829
+ );
3830
+ }
3831
+ /** Загружает число непрочитанных уведомлений. */
3832
+ async count(options = {}) {
3833
+ const body = await this.http.request({
3834
+ method: "GET",
3835
+ path: "/api/notifications/count",
3836
+ ...this.requestOptions(options)
3837
+ });
3838
+ return pickNumber(body, "count", 0);
3839
+ }
3840
+ /**
3841
+ * Отмечает уведомление прочитанным.
3842
+ *
3843
+ * @returns сколько записей отметил сервер
3844
+ */
3845
+ async markRead(notificationId, options = {}) {
3846
+ const body = await this.http.request({
3847
+ method: "POST",
3848
+ path: `/api/notifications/${encodePathSegment(notificationId, "notificationId")}/read`,
3849
+ ...this.requestOptions(options)
3850
+ });
3851
+ return pickNumber(body, "markedCount", 0);
3852
+ }
3853
+ /**
3854
+ * Отмечает прочитанными сразу несколько уведомлений.
3855
+ *
3856
+ * Список автоматически режется на части по 20 идентификаторов — столько же отправляет
3857
+ * сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
3858
+ * результат суммируется.
3859
+ *
3860
+ * @returns сколько записей отметил сервер суммарно
3861
+ */
3862
+ async markReadBatch(ids, options = {}) {
3863
+ let marked = 0;
3864
+ for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
3865
+ const chunk = ids.slice(index, index + READ_BATCH_SIZE);
3866
+ const body = await this.http.request({
3867
+ method: "POST",
3868
+ path: "/api/notifications/read-batch",
3869
+ body: { ids: chunk },
3870
+ ...this.requestOptions(options)
3871
+ });
3872
+ marked += pickNumber(body, "markedCount", 0);
3873
+ }
3874
+ return marked;
3875
+ }
3876
+ /** Отмечает прочитанными все уведомления. */
3877
+ async markAllRead(options = {}) {
3878
+ const body = await this.http.request({
3879
+ method: "POST",
3880
+ path: "/api/notifications/read-all",
3881
+ ...this.requestOptions(options)
3882
+ });
3883
+ return pickNumber(body, "markedCount", 0);
3884
+ }
3885
+ /** Загружает настройки уведомлений. */
3886
+ async getSettings(options = {}) {
3887
+ const body = await this.http.request({
3888
+ method: "GET",
3889
+ path: "/api/notifications/settings",
3890
+ ...this.requestOptions(options)
3891
+ });
3892
+ return readSettings(body);
3893
+ }
3894
+ /**
3895
+ * Обновляет настройки уведомлений.
3896
+ *
3897
+ * Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
3898
+ */
3899
+ async updateSettings(input, options = {}) {
3900
+ const payload = {};
3901
+ for (const key of [
3902
+ "enabled",
3903
+ "sound",
3904
+ "follows",
3905
+ "wallPosts",
3906
+ "likes",
3907
+ "comments",
3908
+ "mentions"
3909
+ ]) {
3910
+ const value = input[key];
3911
+ if (value !== void 0) payload[key] = value;
3912
+ }
3913
+ const body = await this.http.request({
3914
+ method: "PUT",
3915
+ path: "/api/notifications/settings",
3916
+ body: payload,
3917
+ ...this.requestOptions(options)
3918
+ });
3919
+ return readSettings(body);
3920
+ }
3921
+ };
3922
+
3923
+ // src/resources/posts.ts
3924
+ var PostsResource = class extends BaseResource {
3925
+ #uploadFiles;
3926
+ constructor(http, deps) {
3927
+ super(http);
3928
+ this.#uploadFiles = deps.uploadFiles;
3929
+ }
3930
+ /**
3931
+ * Загружает страницу ленты.
3932
+ *
3933
+ * @example
3934
+ * ```ts
3935
+ * const page = await itd.posts.list({ tab: FeedTab.Following, limit: 20 });
3936
+ * const next = await itd.posts.list({ tab: FeedTab.Following, cursor: page.nextCursor ?? undefined });
3937
+ * ```
3938
+ */
3939
+ async list(params = {}) {
3940
+ const body = await this.http.request({
3941
+ method: "GET",
3942
+ path: "/api/posts",
3943
+ query: { tab: params.tab, limit: params.limit, cursor: params.cursor },
3944
+ ...this.requestOptions(params)
3945
+ });
3946
+ return readCursorPage(body, "posts");
3947
+ }
3948
+ /**
3949
+ * Перебирает ленту, сама подставляя курсоры.
3950
+ *
3951
+ * @example
3952
+ * ```ts
3953
+ * for await (const post of itd.posts.iterate({ tab: 'following' })) {
3954
+ * console.log(post.author.username, post.content);
3955
+ * }
3956
+ * ```
3957
+ */
3958
+ iterate(params = {}) {
3959
+ return this.paginate(
3960
+ "cursor",
3961
+ async (state) => {
3962
+ const body = await this.http.request({
3963
+ method: "GET",
3964
+ path: "/api/posts",
3965
+ query: withPageState({ tab: params.tab, limit: params.limit }, state),
3966
+ ...this.requestOptions(params)
3967
+ });
3968
+ return readCursorPage(body, "posts");
3969
+ },
3970
+ params
3971
+ );
3972
+ }
3973
+ /**
3974
+ * Публикует пост.
3975
+ *
3976
+ * Принимает обычный объект, {@link PostBuilder} или функцию-настройщик. Файлы из поля
3977
+ * `files` загружаются автоматически, порядок вложений сохраняется.
3978
+ *
3979
+ * @example
3980
+ * ```ts
3981
+ * await itd.posts.create({ content: 'привет' });
3982
+ * await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
3983
+ * ```
3984
+ */
3985
+ async create(input, options = {}) {
3986
+ const data = resolvePost(input);
3987
+ const attachmentIds = await this.#collectAttachments(data, options);
3988
+ return this.http.request({
3989
+ method: "POST",
3990
+ path: "/api/posts",
3991
+ body: {
3992
+ content: data.content ?? "",
3993
+ ...data.spans ? { spans: data.spans } : {},
3994
+ ...data.wallRecipientId ? { wallRecipientId: data.wallRecipientId } : {},
3995
+ ...attachmentIds.length > 0 ? { attachmentIds } : {},
3996
+ ...data.poll ? { poll: data.poll } : {}
3997
+ },
3998
+ ...this.requestOptions(options)
3999
+ });
4000
+ }
4001
+ /**
4002
+ * Загружает один пост вместе с топовыми комментариями.
4003
+ *
4004
+ * В отличие от списков, здесь у поста заполнено поле `comments`.
4005
+ */
4006
+ get(postId, options = {}) {
4007
+ return this.http.request({
4008
+ method: "GET",
4009
+ path: `/api/posts/${encodePathSegment(postId, "postId")}`,
4010
+ ...this.requestOptions(options)
4011
+ });
4012
+ }
4013
+ /** Редактирует текст поста. */
4014
+ update(postId, input, options = {}) {
4015
+ return this.http.request({
4016
+ method: "PUT",
4017
+ path: `/api/posts/${encodePathSegment(postId, "postId")}`,
4018
+ body: { content: input.content ?? "", ...input.spans ? { spans: input.spans } : {} },
4019
+ ...this.requestOptions(options)
4020
+ });
4021
+ }
4022
+ /** Удаляет пост. Восстановить его можно через {@link restore}. */
4023
+ remove(postId, options = {}) {
4024
+ return this.http.request({
4025
+ method: "DELETE",
4026
+ path: `/api/posts/${encodePathSegment(postId, "postId")}`,
4027
+ ...this.requestOptions(options)
4028
+ });
4029
+ }
4030
+ /** Восстанавливает удалённый пост. */
4031
+ restore(postId, options = {}) {
4032
+ return this.http.request({
4033
+ method: "POST",
4034
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/restore`,
4035
+ ...this.requestOptions(options)
4036
+ });
4037
+ }
4038
+ /** Ставит реакцию на пост. */
4039
+ like(postId, options = {}) {
4040
+ return this.http.request({
4041
+ method: "POST",
4042
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/like`,
4043
+ ...this.requestOptions(options)
4044
+ });
4045
+ }
4046
+ /** Убирает реакцию с поста. */
4047
+ unlike(postId, options = {}) {
4048
+ return this.http.request({
4049
+ method: "DELETE",
4050
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/like`,
4051
+ ...this.requestOptions(options)
4052
+ });
4053
+ }
4054
+ /**
4055
+ * Делает репост с необязательным комментарием.
4056
+ *
4057
+ * Вложения к репосту не поддерживаются: сервер их игнорирует, поэтому параметров
4058
+ * для файлов здесь нет.
4059
+ */
4060
+ repost(postId, content = "", options = {}) {
4061
+ return this.http.request({
4062
+ method: "POST",
4063
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/repost`,
4064
+ body: { content },
4065
+ ...this.requestOptions(options)
4066
+ });
4067
+ }
4068
+ /** Отменяет репост. */
4069
+ unrepost(postId, options = {}) {
4070
+ return this.http.request({
4071
+ method: "DELETE",
4072
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/repost`,
4073
+ ...this.requestOptions(options)
4074
+ });
4075
+ }
4076
+ /** Закрепляет пост в профиле. */
4077
+ pin(postId, options = {}) {
4078
+ return this.http.request({
4079
+ method: "POST",
4080
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/pin`,
4081
+ ...this.requestOptions(options)
4082
+ });
4083
+ }
4084
+ /** Открепляет пост. */
4085
+ unpin(postId, options = {}) {
4086
+ return this.http.request({
4087
+ method: "DELETE",
4088
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/pin`,
4089
+ ...this.requestOptions(options)
4090
+ });
4091
+ }
4092
+ /**
4093
+ * Голосует в опросе.
4094
+ *
4095
+ * @param optionIds выбранные варианты; несколько допустимы только при `multipleChoice`
4096
+ */
4097
+ vote(postId, optionIds, options = {}) {
4098
+ return this.http.request({
4099
+ method: "POST",
4100
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/poll/vote`,
4101
+ body: { optionIds },
4102
+ ...this.requestOptions(options)
4103
+ });
4104
+ }
4105
+ /** Запрашивает счётчики сразу для нескольких постов. */
4106
+ async stats(ids, options = {}) {
4107
+ const body = await this.http.request({
4108
+ method: "POST",
4109
+ path: "/api/posts/stats",
4110
+ body: { ids },
4111
+ ...this.requestOptions(options)
4112
+ });
4113
+ return pickArray(body, "posts");
4114
+ }
4115
+ /** Загружает страницу постов пользователя (его стену). */
4116
+ async byUser(user, params = {}) {
4117
+ const body = await this.http.request({
4118
+ method: "GET",
4119
+ path: `/api/posts/user/${encodePathSegment(user, "user")}`,
4120
+ query: {
4121
+ limit: params.limit,
4122
+ cursor: params.cursor,
4123
+ sort: params.sort,
4124
+ pinnedPostId: params.pinnedPostId
4125
+ },
4126
+ ...this.requestOptions(params)
4127
+ });
4128
+ return readCursorPage(body, "posts");
4129
+ }
4130
+ /** Перебирает посты пользователя. */
4131
+ iterateByUser(user, params = {}) {
4132
+ const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4133
+ return this.paginate(
4134
+ "cursor",
4135
+ async (state) => {
4136
+ const body = await this.http.request({
4137
+ method: "GET",
4138
+ path,
4139
+ query: withPageState(
4140
+ { limit: params.limit, sort: params.sort, pinnedPostId: params.pinnedPostId },
4141
+ state
4142
+ ),
4143
+ ...this.requestOptions(params)
4144
+ });
4145
+ return readCursorPage(body, "posts");
4146
+ },
4147
+ params
4148
+ );
4149
+ }
4150
+ /** Загружает страницу постов, которые пользователь отметил реакцией. */
4151
+ async likedByUser(user, params = {}) {
4152
+ const body = await this.http.request({
4153
+ method: "GET",
4154
+ path: `/api/posts/user/${encodePathSegment(user, "user")}/liked`,
4155
+ query: { limit: params.limit, cursor: params.cursor },
4156
+ ...this.requestOptions(params)
4157
+ });
4158
+ return readCursorPage(body, "posts");
4159
+ }
4160
+ /** Перебирает посты, которые пользователь отметил реакцией. */
4161
+ iterateLikedByUser(user, params = {}) {
4162
+ const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
4163
+ return this.paginate(
4164
+ "cursor",
4165
+ async (state) => {
4166
+ const body = await this.http.request({
4167
+ method: "GET",
4168
+ path,
4169
+ query: withPageState({ limit: params.limit }, state),
4170
+ ...this.requestOptions(params)
4171
+ });
4172
+ return readCursorPage(body, "posts");
4173
+ },
4174
+ params
4175
+ );
4176
+ }
4177
+ /**
4178
+ * Загружает страницу комментариев к посту.
4179
+ *
4180
+ * У этого эндпоинта курсор и признак продолжения лежат рядом со списком, а не внутри
4181
+ * объекта `pagination`, как у остальных, — разница скрыта внутри.
4182
+ */
4183
+ async comments(postId, params = {}) {
4184
+ const body = await this.http.request({
4185
+ method: "GET",
4186
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
4187
+ query: { limit: params.limit, cursor: params.cursor, sort: params.sort },
4188
+ ...this.requestOptions(params)
4189
+ });
4190
+ return readFlatCursorPage(body, "comments");
4191
+ }
4192
+ /** Перебирает комментарии к посту. */
4193
+ iterateComments(postId, params = {}) {
4194
+ const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
4195
+ return this.paginate(
4196
+ "cursor",
4197
+ async (state) => {
4198
+ const body = await this.http.request({
4199
+ method: "GET",
4200
+ path,
4201
+ query: withPageState({ limit: params.limit, sort: params.sort }, state),
4202
+ ...this.requestOptions(params)
4203
+ });
4204
+ return readFlatCursorPage(body, "comments");
4205
+ },
4206
+ params
4207
+ );
4208
+ }
4209
+ /**
4210
+ * Комментирует пост.
4211
+ *
4212
+ * @example
4213
+ * ```ts
4214
+ * await itd.posts.comment(postId, 'согласен');
4215
+ * await itd.posts.comment(postId, (c) => c.content('смотри').attach('./meme.png'));
4216
+ * ```
4217
+ */
4218
+ async comment(postId, input, options = {}) {
4219
+ const data = resolveComment(typeof input === "string" ? { content: input } : input);
4220
+ const attachmentIds = await this.#collectAttachments(data, options);
4221
+ return this.http.request({
4222
+ method: "POST",
4223
+ path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
4224
+ body: { content: data.content ?? "", attachmentIds },
4225
+ ...this.requestOptions(options)
4226
+ });
4227
+ }
4228
+ /**
4229
+ * Отправляет голосовой комментарий.
4230
+ *
4231
+ * Текста у такого комментария нет: сервер ждёт пустой `content` и одно аудиовложение
4232
+ * в формате `audio/ogg`.
4233
+ *
4234
+ * @example
4235
+ * ```ts
4236
+ * await itd.posts.voiceComment(postId, './answer.ogg');
4237
+ * ```
4238
+ */
4239
+ voiceComment(postId, audio, options = {}) {
4240
+ return this.comment(postId, { content: "", files: [audio] }, options);
4241
+ }
4242
+ /** Загружает файлы из входных данных и объединяет их с уже готовыми идентификаторами. */
4243
+ async #collectAttachments(data, options) {
4244
+ const existing = data.attachmentIds ?? [];
4245
+ const files = data.files ?? [];
4246
+ if (files.length === 0) return existing;
4247
+ const uploaded = await this.#uploadFiles(files, options);
4248
+ return [...existing, ...uploaded];
4249
+ }
4250
+ };
4251
+
4252
+ // src/resources/users.ts
4253
+ var UsersResource = class extends BaseResource {
4254
+ /** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
4255
+ me(options = {}) {
4256
+ return this.http.request({
4257
+ method: "GET",
4258
+ path: "/api/users/me",
4259
+ ...this.requestOptions(options)
4260
+ });
4261
+ }
4262
+ /** Обновляет свой профиль. Передавайте только изменяемые поля. */
4263
+ updateMe(input, options = {}) {
4264
+ return this.http.request({
4265
+ method: "PUT",
4266
+ path: "/api/users/me",
4267
+ body: input,
4268
+ ...this.requestOptions(options)
4269
+ });
4270
+ }
4271
+ /** Деактивирует аккаунт. Вернуть его можно через {@link restore}. */
4272
+ deactivate(options = {}) {
4273
+ return this.http.request({
4274
+ method: "DELETE",
4275
+ path: "/api/users/me",
4276
+ ...this.requestOptions(options)
4277
+ });
4278
+ }
4279
+ /** Восстанавливает деактивированный аккаунт. */
4280
+ restore(options = {}) {
4281
+ return this.http.request({
4282
+ method: "POST",
4283
+ path: "/api/users/me/restore",
4284
+ ...this.requestOptions(options)
4285
+ });
4286
+ }
4287
+ /** Создаёт профиль после регистрации. */
4288
+ createProfile(input, options = {}) {
4289
+ return this.http.request({
4290
+ method: "POST",
4291
+ path: "/api/users/profile",
4292
+ body: input,
4293
+ ...this.requestOptions(options)
4294
+ });
4295
+ }
4296
+ /**
4297
+ * Загружает профиль пользователя.
4298
+ *
4299
+ * @param user UUID **или** имя пользователя — подходит и то, и другое
4300
+ *
4301
+ * @example
4302
+ * ```ts
4303
+ * const profile = await itd.users.get('durov');
4304
+ * await itd.posts.create({ content: 'привет', wallRecipientId: profile.id });
4305
+ * ```
4306
+ */
4307
+ get(user, options = {}) {
4308
+ return this.http.request({
4309
+ method: "GET",
4310
+ path: `/api/users/${encodePathSegment(user, "user")}`,
4311
+ ...this.requestOptions(options)
4312
+ });
4313
+ }
4314
+ /** Проверяет, свободно ли имя пользователя. */
4315
+ async checkUsername(username, options = {}) {
4316
+ const body = await this.http.request({
4317
+ method: "GET",
4318
+ path: "/api/users/check-username",
4319
+ query: { username },
4320
+ ...this.requestOptions(options)
4321
+ });
4322
+ return pickBoolean(body, "available");
4323
+ }
4324
+ /** Ищет пользователей по строке запроса. */
4325
+ async search(query, params = {}) {
4326
+ const body = await this.http.request({
4327
+ method: "GET",
4328
+ path: "/api/users/search",
4329
+ query: { q: query, limit: params.limit },
4330
+ ...this.requestOptions(params)
4331
+ });
4332
+ return pickArray(body, "users");
4333
+ }
4334
+ /** Загружает рекомендации, на кого подписаться. */
4335
+ async whoToFollow(options = {}) {
4336
+ const body = await this.http.request({
4337
+ method: "GET",
4338
+ path: "/api/users/suggestions/who-to-follow",
4339
+ ...this.requestOptions(options)
4340
+ });
4341
+ return pickArray(body, "users");
4342
+ }
4343
+ /** Загружает рейтинг кланов. */
4344
+ async topClans(options = {}) {
4345
+ const body = await this.http.request({
4346
+ method: "GET",
4347
+ path: "/api/users/stats/top-clans",
4348
+ ...this.requestOptions(options)
4349
+ });
4350
+ return pickArray(body, "clans");
4351
+ }
4352
+ /**
4353
+ * Подписывается на пользователя.
4354
+ *
4355
+ * У закрытого профиля вместо подписки отправляется заявка — это видно по полю `status`.
4356
+ */
4357
+ follow(user, options = {}) {
4358
+ return this.http.request({
4359
+ method: "POST",
4360
+ path: `/api/users/${encodePathSegment(user, "user")}/follow`,
4361
+ body: {},
4362
+ ...this.requestOptions(options)
4363
+ });
4364
+ }
4365
+ /** Отписывается от пользователя. */
4366
+ unfollow(user, options = {}) {
4367
+ return this.http.request({
4368
+ method: "DELETE",
4369
+ path: `/api/users/${encodePathSegment(user, "user")}/follow`,
4370
+ ...this.requestOptions(options)
4371
+ });
4372
+ }
4373
+ /** Загружает страницу подписчиков. */
4374
+ followers(user, params = {}) {
4375
+ return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4376
+ }
4377
+ /** Перебирает подписчиков. */
4378
+ iterateFollowers(user, params = {}) {
4379
+ return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4380
+ }
4381
+ /** Загружает страницу подписок. */
4382
+ following(user, params = {}) {
4383
+ return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4384
+ }
4385
+ /** Перебирает подписки. */
4386
+ iterateFollowing(user, params = {}) {
4387
+ return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4388
+ }
4389
+ /**
4390
+ * Проверяет, подписаны ли вы, сразу для нескольких пользователей.
4391
+ *
4392
+ * @returns объект «идентификатор пользователя → подписаны ли вы»
4393
+ *
4394
+ * @example
4395
+ * ```ts
4396
+ * const statuses = await itd.users.followStatus([userA, userB]);
4397
+ * // { 'b89dee4f-…': true, '35ea3059-…': false }
4398
+ * ```
4399
+ */
4400
+ followStatus(userIds, options = {}) {
4401
+ return this.http.request({
4402
+ method: "POST",
4403
+ path: "/api/users/follow-status",
4404
+ body: { userIds },
4405
+ ...this.requestOptions(options)
4406
+ });
4407
+ }
4408
+ /** Блокирует пользователя. */
4409
+ block(user, options = {}) {
4410
+ return this.http.request({
4411
+ method: "POST",
4412
+ path: `/api/users/${encodePathSegment(user, "user")}/block`,
4413
+ body: {},
4414
+ ...this.requestOptions(options)
4415
+ });
4416
+ }
4417
+ /** Снимает блокировку. */
4418
+ unblock(user, options = {}) {
4419
+ return this.http.request({
4420
+ method: "DELETE",
4421
+ path: `/api/users/${encodePathSegment(user, "user")}/block`,
4422
+ ...this.requestOptions(options)
4423
+ });
4424
+ }
4425
+ /** Загружает страницу заблокированных пользователей. */
4426
+ blocked(params = {}) {
4427
+ return this.#userPage("/api/users/me/blocked", params);
4428
+ }
4429
+ /** Перебирает заблокированных пользователей. */
4430
+ iterateBlocked(params = {}) {
4431
+ return this.#userPaginator("/api/users/me/blocked", params);
4432
+ }
4433
+ /** Загружает настройки приватности. */
4434
+ getPrivacy(options = {}) {
4435
+ return this.http.request({
4436
+ method: "GET",
4437
+ path: "/api/users/me/privacy",
4438
+ ...this.requestOptions(options)
4439
+ });
4440
+ }
4441
+ /** Обновляет настройки приватности. Передавайте только изменяемые поля. */
4442
+ updatePrivacy(input, options = {}) {
4443
+ return this.http.request({
4444
+ method: "PUT",
4445
+ path: "/api/users/me/privacy",
4446
+ body: input,
4447
+ ...this.requestOptions(options)
4448
+ });
4449
+ }
4450
+ /**
4451
+ * Загружает значки профиля и выбранный из них.
4452
+ *
4453
+ * `activePin` — строка-идентификатор, а не объект.
4454
+ */
4455
+ async pins(options = {}) {
4456
+ const body = await this.http.request({
4457
+ method: "GET",
4458
+ path: "/api/users/me/pins",
4459
+ ...this.requestOptions(options)
4460
+ });
4461
+ return {
4462
+ pins: pickArray(body, "pins"),
4463
+ // Сервер отдаёт здесь строку-идентификатор, а не объект значка.
4464
+ activePin: pickString(body, "activePin") ?? null
4465
+ };
4466
+ }
4467
+ /** Выбирает активный значок профиля. */
4468
+ setPin(slug, options = {}) {
4469
+ return this.http.request({
4470
+ method: "PUT",
4471
+ path: "/api/users/me/pin",
4472
+ body: { slug },
4473
+ ...this.requestOptions(options)
4474
+ });
4475
+ }
4476
+ /** Снимает активный значок. */
4477
+ removePin(options = {}) {
4478
+ return this.http.request({
4479
+ method: "DELETE",
4480
+ path: "/api/users/me/pin",
4481
+ ...this.requestOptions(options)
4482
+ });
4483
+ }
4484
+ async #userPage(path, params) {
4485
+ const body = await this.http.request({
4486
+ method: "GET",
4487
+ path,
4488
+ query: { limit: params.limit, page: params.page },
4489
+ ...this.requestOptions(params)
4490
+ });
4491
+ return readPagedPage(body, "users");
4492
+ }
4493
+ #userPaginator(path, params) {
4494
+ return this.paginate(
4495
+ "page",
4496
+ async (state) => {
4497
+ const body = await this.http.request({
4498
+ method: "GET",
4499
+ path,
4500
+ query: withPageState({ limit: params.limit }, state),
4501
+ ...this.requestOptions(params)
4502
+ });
4503
+ return readPagedPage(body, "users");
4504
+ },
4505
+ params
4506
+ );
4507
+ }
4508
+ };
4509
+
4510
+ // src/client.ts
4511
+ var ItdClient = class {
4512
+ #config;
4513
+ #http;
4514
+ #authManager;
4515
+ #jar;
4516
+ #queue;
4517
+ /** Авторизация, сессии и пароли. */
4518
+ auth;
4519
+ /** Профили, подписки, блокировки, приватность. */
4520
+ users;
4521
+ /** Лента, публикация, реакции, репосты, комментарии к постам. */
4522
+ posts;
4523
+ /** Ответы на комментарии и действия над ними. */
4524
+ comments;
4525
+ /** Загрузка файлов и медиа. */
4526
+ files;
4527
+ /** Уведомления: список, счётчик, отметки о прочтении, настройки. */
4528
+ notifications;
4529
+ /** Хэштеги и посты по ним. */
4530
+ hashtags;
4531
+ /** Глобальный поиск по пользователям и хэштегам. */
4532
+ search;
4533
+ /** Жалобы на контент и пользователей. */
4534
+ reports;
4535
+ /** Верификация профиля. */
4536
+ verification;
4537
+ /** Подписка и способы оплаты. */
4538
+ subscription;
4539
+ /** Сведения о платформе: изменения, анонсы, баннер события. */
4540
+ platform;
4541
+ /**
4542
+ * Телеметрия просмотров.
4543
+ *
4544
+ * @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
4545
+ */
4546
+ telemetry;
4547
+ constructor(options = {}) {
4548
+ this.#config = resolveConfig(options);
4549
+ this.#jar = new CookieJar();
4550
+ this.#http = new HttpClient(this.#config);
4551
+ this.#authManager = new AuthManager(this.#config, this.#http, this.#jar);
4552
+ this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
4553
+ this.#http.setCollaborators({
4554
+ getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4555
+ onUnauthorized: () => this.#authManager.onUnauthorized(),
4556
+ getCookieHeader: (url) => this.#jar.getHeader(url),
4557
+ saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
4558
+ ...this.#queue ? { schedule: this.#queue.schedule.bind(this.#queue) } : {},
4559
+ // Планировщик нужен, даже когда обычные повторы выключены: лимит частоты
4560
+ // живёт по своим правилам и настраивается отдельно, в `rateLimit`.
4561
+ ...this.#config.retry || this.#config.rateLimit ? { nextRetryDelay: this.#createRetryScheduler() } : {},
4562
+ ...this.#queue && this.#config.rateLimit?.respectHeaders ? { onRateLimit: this.#throttleByHeaders.bind(this) } : {}
4563
+ });
4564
+ this.files = new FilesResource(this.#http);
4565
+ const uploadFiles = (files, requestOptions) => this.files.uploadMany(files, requestOptions ?? {});
4566
+ this.auth = new AuthResource(this.#http, { auth: this.#authManager });
4567
+ this.users = new UsersResource(this.#http);
4568
+ this.posts = new PostsResource(this.#http, { uploadFiles });
4569
+ this.comments = new CommentsResource(this.#http, { uploadFiles });
4570
+ this.notifications = new NotificationsResource(this.#http);
4571
+ this.hashtags = new HashtagsResource(this.#http);
4572
+ this.search = new SearchResource(this.#http);
4573
+ this.reports = new ReportsResource(this.#http);
4574
+ this.verification = new VerificationResource(this.#http);
4575
+ this.subscription = new SubscriptionResource(this.#http);
4576
+ this.platform = new PlatformResource(this.#http);
4577
+ this.telemetry = new TelemetryResource(this.#http);
4578
+ }
4579
+ /** Базовый URL, к которому обращается клиент. */
4580
+ get baseUrl() {
4581
+ return this.#config.baseUrl;
4582
+ }
4583
+ /**
4584
+ * Выполняет произвольный запрос к API.
4585
+ *
4586
+ * Запасной путь для случаев, когда нужного метода ещё нет или ответ сервера разошёлся
4587
+ * с документацией. Проходит через ту же авторизацию, очередь и обработку ошибок.
4588
+ *
4589
+ * @example
4590
+ * ```ts
4591
+ * const raw = await itd.request({ method: 'GET', path: '/api/posts', raw: true });
4592
+ * ```
4593
+ */
4594
+ request(options) {
4595
+ return this.#http.request(options);
4596
+ }
4597
+ /**
4598
+ * Подписывается на события авторизации.
4599
+ *
4600
+ * Полезно, чтобы сохранять сессию во внешнее хранилище или узнавать, что вход
4601
+ * окончательно потерян.
4602
+ *
4603
+ * @returns функция отписки
4604
+ *
4605
+ * @example
4606
+ * ```ts
4607
+ * itd.on('tokens', ({ accessToken }) => cache.set('itd', accessToken));
4608
+ * itd.on('authError', () => notifyUser('Сессия истекла, войдите заново'));
4609
+ * ```
4610
+ */
4611
+ on(event, listener) {
4612
+ return this.#authManager.on(event, listener);
4613
+ }
4614
+ /**
4615
+ * Создаёт поток уведомлений в реальном времени.
4616
+ *
4617
+ * Каждый вызов даёт новый независимый поток; обычно он нужен один на приложение.
4618
+ * Соединение поднимается методом `connect()` и держится само.
4619
+ *
4620
+ * @example
4621
+ * ```ts
4622
+ * const stream = itd.realtime();
4623
+ *
4624
+ * stream.on('notification', ({ notification }) => {
4625
+ * console.log(formatNotificationText(notification));
4626
+ * });
4627
+ * stream.on('unreadCount', (count) => setBadge(count));
4628
+ *
4629
+ * await stream.connect();
4630
+ * ```
4631
+ */
4632
+ realtime(options = {}) {
4633
+ return new ItdRealtime(
4634
+ {
4635
+ baseUrl: this.#config.baseUrl,
4636
+ fetch: this.#config.fetch,
4637
+ getToken: () => this.#authManager.getAccessToken(),
4638
+ refresh: () => this.#authManager.onUnauthorized(),
4639
+ fetchUnreadCount: () => this.notifications.count(),
4640
+ logger: this.#config.logger
4641
+ },
4642
+ options
4643
+ );
4644
+ }
4645
+ /** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
4646
+ getSession() {
4647
+ return this.#authManager.getSession();
4648
+ }
4649
+ /** Восстанавливает сохранённую сессию, включая cookie. */
4650
+ setSession(session) {
4651
+ return this.#authManager.setSession(session);
4652
+ }
4653
+ /**
4654
+ * Подключает чтение файлов с диска.
4655
+ *
4656
+ * Вызывается из `itd-api/node`; напрямую обычно не нужно.
4657
+ *
4658
+ * @internal
4659
+ */
4660
+ setFileReader(readFile) {
4661
+ this.files.setFileReader(readFile);
4662
+ }
4663
+ /**
4664
+ * Придерживает очередь, когда лимит сервера исчерпан.
4665
+ *
4666
+ * Сервер сообщает остаток в заголовке `x-ratelimit-remaining`. Как только тот доходит
4667
+ * до нуля, очередь встаёт на первую паузу лестницы — короткую, потому что окно могло
4668
+ * почти истечь. Если оно ещё действует, следующий запрос получит `429`, и дальше
4669
+ * лестницу отработает планировщик повторов.
4670
+ *
4671
+ * Смысл этой паузы прежде всего в том, чтобы при работе в несколько потоков остальные
4672
+ * запросы не улетели в стену все разом.
4673
+ */
4674
+ #throttleByHeaders(limit, remaining) {
4675
+ if (remaining === void 0 || remaining > 0) return;
4676
+ const first = this.#config.rateLimit?.retryDelays[0];
4677
+ if (first === void 0) return;
4678
+ this.#queue?.pause(first);
4679
+ this.#config.logger?.debug(
4680
+ `\u043B\u0438\u043C\u0438\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${remaining} \u0438\u0437 ${limit ?? "?"}), \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0436\u0434\u0451\u0442 ${first} \u043C\u0441`
4681
+ );
4682
+ }
4683
+ /**
4684
+ * Собирает планировщик повторов и связывает его с очередью.
4685
+ *
4686
+ * Ответ `429` обрабатывается отдельно от прочих ошибок. Причина в том, что сервер
4687
+ * не присылает `Retry-After` и не сообщает время сброса окна: экспоненциальный откат
4688
+ * в сотни миллисекунд здесь бесполезен, а окно измеряется десятками секунд. Вместо
4689
+ * расчёта берётся лестница пауз `rateLimit.retryDelays`, и она не зависит
4690
+ * от `retry.attempts`, у которого совсем другая задача.
4691
+ *
4692
+ * Пауза накладывается на всю очередь: иначе остальные запросы продолжат добивать API,
4693
+ * пока первый ждёт.
4694
+ */
4695
+ #createRetryScheduler() {
4696
+ const retry = this.#config.retry;
4697
+ const scheduler = retry ? createRetryScheduler(retry) : void 0;
4698
+ const queue = this.#queue;
4699
+ const delays = this.#config.rateLimit?.retryDelays ?? [];
4700
+ return (error, attempt, method) => {
4701
+ if (isItdRateLimitError(error)) {
4702
+ const wait = error.retryAfter ?? delays[attempt - 1];
4703
+ if (wait === void 0) return void 0;
4704
+ queue?.pause(wait);
4705
+ this.#config.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
4706
+ return wait;
4707
+ }
4708
+ return scheduler?.(error, attempt, method);
4709
+ };
4710
+ }
4711
+ };
4712
+ function createClient(options = {}) {
4713
+ return new ItdClient(options);
4714
+ }
4715
+
4716
+ // src/notifications/text.ts
4717
+ var UNKNOWN_ACTOR = "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C";
4718
+ var FALLBACK_TEXT = "\u041D\u043E\u0432\u043E\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0435";
4719
+ var TEMPLATES = Object.freeze({
4720
+ [NotificationType.Follow]: {
4721
+ one: (name) => `${name} \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043B\u0441\u044F(-\u0430\u0441\u044C) \u043D\u0430 \u0432\u0430\u0441`,
4722
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043B\u0438\u0441\u044C \u043D\u0430 \u0432\u0430\u0441`
4723
+ },
4724
+ [NotificationType.FollowRequest]: {
4725
+ one: (name) => `${name} \u0445\u043E\u0447\u0435\u0442 \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0432\u0430\u0441`,
4726
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u0445\u043E\u0442\u044F\u0442 \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0432\u0430\u0441`
4727
+ },
4728
+ [NotificationType.FollowAccepted]: {
4729
+ one: (name) => `${name} \u043F\u0440\u0438\u043D\u044F\u043B(\u0430) \u0432\u0430\u0448\u0443 \u0437\u0430\u044F\u0432\u043A\u0443`,
4730
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043F\u0440\u0438\u043D\u044F\u043B\u0438 \u0432\u0430\u0448\u0443 \u0437\u0430\u044F\u0432\u043A\u0443`
4731
+ },
4732
+ [NotificationType.PostReaction]: {
4733
+ one: (name) => `${name} \u043E\u0446\u0435\u043D\u0438\u043B(\u0430) \u0432\u0430\u0448 \u043F\u043E\u0441\u0442`,
4734
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043E\u0446\u0435\u043D\u0438\u043B\u0438 \u0432\u0430\u0448 \u043F\u043E\u0441\u0442`
4735
+ },
4736
+ [NotificationType.PostComment]: {
4737
+ one: (name) => `${name} \u043F\u0440\u043E\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043B(\u0430) \u0432\u0430\u0448 \u043F\u043E\u0441\u0442`,
4738
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043F\u0440\u043E\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043B\u0438 \u0432\u0430\u0448 \u043F\u043E\u0441\u0442`
4739
+ },
4740
+ [NotificationType.PostRepost]: {
4741
+ one: (name) => `${name} \u0441\u0434\u0435\u043B\u0430\u043B(\u0430) \u0440\u0435\u043F\u043E\u0441\u0442`,
4742
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u0441\u0434\u0435\u043B\u0430\u043B\u0438 \u0440\u0435\u043F\u043E\u0441\u0442`
4743
+ },
4744
+ [NotificationType.CommentReaction]: {
4745
+ one: (name) => `${name} \u043E\u0446\u0435\u043D\u0438\u043B(\u0430) \u0432\u0430\u0448 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439`,
4746
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043E\u0446\u0435\u043D\u0438\u043B\u0438 \u0432\u0430\u0448 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439`
4747
+ },
4748
+ [NotificationType.CommentReply]: {
4749
+ one: (name) => `${name} \u043E\u0442\u0432\u0435\u0442\u0438\u043B(\u0430) \u043D\u0430 \u0432\u0430\u0448 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439`,
4750
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043E\u0442\u0432\u0435\u0442\u0438\u043B\u0438 \u043D\u0430 \u0432\u0430\u0448 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439`
4751
+ },
4752
+ [NotificationType.PostMention]: {
4753
+ one: (name) => `${name} \u0443\u043F\u043E\u043C\u044F\u043D\u0443\u043B(\u0430) \u0432\u0430\u0441 \u0432 \u043F\u043E\u0441\u0442\u0435`,
4754
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u0443\u043F\u043E\u043C\u044F\u043D\u0443\u043B\u0438 \u0432\u0430\u0441 \u0432 \u043F\u043E\u0441\u0442\u0435`
4755
+ },
4756
+ [NotificationType.CommentMention]: {
4757
+ one: (name) => `${name} \u0443\u043F\u043E\u043C\u044F\u043D\u0443\u043B(\u0430) \u0432\u0430\u0441 \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438`,
4758
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u0443\u043F\u043E\u043C\u044F\u043D\u0443\u043B\u0438 \u0432\u0430\u0441 \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438`
4759
+ },
4760
+ [NotificationType.WallPost]: {
4761
+ one: (name) => `${name} \u043D\u0430\u043F\u0438\u0441\u0430\u043B(\u0430) \u043D\u0430 \u0432\u0430\u0448\u0435\u0439 \u0441\u0442\u0435\u043D\u0435`,
4762
+ many: (name, others) => `${name} \u0438 \u0435\u0449\u0451 ${others} \u043D\u0430\u043F\u0438\u0441\u0430\u043B\u0438 \u043D\u0430 \u0432\u0430\u0448\u0435\u0439 \u0441\u0442\u0435\u043D\u0435`
4763
+ },
4764
+ [NotificationType.VerificationApproved]: {
4765
+ one: () => "\u0412\u0430\u0448\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 \u043D\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0430",
4766
+ many: () => "\u0412\u0430\u0448\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 \u043D\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0430"
4767
+ },
4768
+ [NotificationType.VerificationRejected]: {
4769
+ one: () => "\u0412\u0430\u0448\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 \u043D\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430",
4770
+ many: () => "\u0412\u0430\u0448\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 \u043D\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430"
4771
+ }
4772
+ });
4773
+ function formatNotificationText(notification) {
4774
+ const template = TEMPLATES[notification.type];
4775
+ if (!template) return FALLBACK_TEXT;
4776
+ const first = notification.actors[0];
4777
+ const name = first?.displayName || first?.username || UNKNOWN_ACTOR;
4778
+ const others = notification.count - 1;
4779
+ return others > 0 ? template.many(name, others) : template.one(name);
4780
+ }
4781
+
4782
+ // src/notifications/url.ts
4783
+ var POST_TYPES = /* @__PURE__ */ new Set([
4784
+ NotificationType.PostReaction,
4785
+ NotificationType.PostRepost,
4786
+ NotificationType.PostMention,
4787
+ NotificationType.WallPost
4788
+ ]);
4789
+ var COMMENT_TYPES = /* @__PURE__ */ new Set([
4790
+ NotificationType.PostComment,
4791
+ NotificationType.CommentReaction,
4792
+ NotificationType.CommentReply,
4793
+ NotificationType.CommentMention
4794
+ ]);
4795
+ var FOLLOW_TYPES = /* @__PURE__ */ new Set([
4796
+ NotificationType.Follow,
4797
+ NotificationType.FollowRequest,
4798
+ NotificationType.FollowAccepted
4799
+ ]);
4800
+ function resolveNotificationUrl(notification) {
4801
+ const { type, entityId, parentEntityId, clickUrl } = notification;
4802
+ const username = notification.actors[0]?.username;
4803
+ if (username && entityId) {
4804
+ if (POST_TYPES.has(type)) return `/@${username}/post/${entityId}`;
4805
+ if (COMMENT_TYPES.has(type)) {
4806
+ return parentEntityId ? `/@${username}/post/${parentEntityId}?comment=${entityId}` : `/@${username}/post/${entityId}`;
4807
+ }
4808
+ }
4809
+ if (username && FOLLOW_TYPES.has(type)) return `/@${username}`;
4810
+ return clickUrl || "/notifications";
4811
+ }
4812
+
4813
+ // src/types/models.ts
4814
+ function isMyProfile(profile) {
4815
+ return "subscription" in profile;
4816
+ }
4817
+ function toDate(value) {
4818
+ if (!value) return null;
4819
+ const date = new Date(value);
4820
+ return Number.isFinite(date.getTime()) ? date : null;
4821
+ }
4822
+
4823
+ exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
4824
+ exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
4825
+ exports.AttachmentType = AttachmentType;
4826
+ exports.CommentSort = CommentSort;
4827
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
4828
+ exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
4829
+ exports.FeedTab = FeedTab;
4830
+ exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
4831
+ exports.ItdAbortError = ItdAbortError;
4832
+ exports.ItdApiError = ItdApiError;
4833
+ exports.ItdAuthError = ItdAuthError;
4834
+ exports.ItdClient = ItdClient;
4835
+ exports.ItdConfigError = ItdConfigError;
4836
+ exports.ItdConflictError = ItdConflictError;
4837
+ exports.ItdError = ItdError;
4838
+ exports.ItdErrorCode = ItdErrorCode;
4839
+ exports.ItdForbiddenError = ItdForbiddenError;
4840
+ exports.ItdNetworkError = ItdNetworkError;
4841
+ exports.ItdNotFoundError = ItdNotFoundError;
4842
+ exports.ItdPhoneVerificationError = ItdPhoneVerificationError;
4843
+ exports.ItdRateLimitError = ItdRateLimitError;
4844
+ exports.ItdRealtime = ItdRealtime;
4845
+ exports.ItdServerError = ItdServerError;
4846
+ exports.ItdTimeoutError = ItdTimeoutError;
4847
+ exports.ItdValidationError = ItdValidationError;
4848
+ exports.LikesVisibility = LikesVisibility;
4849
+ exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
4850
+ exports.MAX_RECONNECT_ATTEMPTS = MAX_RECONNECT_ATTEMPTS;
4851
+ exports.MemoryTokenStorage = MemoryTokenStorage;
4852
+ exports.NOTIFICATION_TYPE_ALIASES = NOTIFICATION_TYPE_ALIASES;
4853
+ exports.NotificationType = NotificationType;
4854
+ exports.Paginator = Paginator;
4855
+ exports.RECONNECT_BACKOFF = RECONNECT_BACKOFF;
4856
+ exports.RECONNECT_JITTER = RECONNECT_JITTER;
4857
+ exports.RealtimeStatus = RealtimeStatus;
4858
+ exports.ReportReason = ReportReason;
4859
+ exports.ReportTargetType = ReportTargetType;
4860
+ exports.STREAM_PATH = STREAM_PATH;
4861
+ exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
4862
+ exports.WallAccess = WallAccess;
4863
+ exports.canonicalNotificationType = canonicalNotificationType;
4864
+ exports.comment = comment;
4865
+ exports.createClient = createClient;
4866
+ exports.createTokenStorage = createTokenStorage;
4867
+ exports.formatNotificationText = formatNotificationText;
4868
+ exports.isBuilder = isBuilder;
4869
+ exports.isItdApiError = isItdApiError;
4870
+ exports.isItdAuthError = isItdAuthError;
4871
+ exports.isItdError = isItdError;
4872
+ exports.isItdRateLimitError = isItdRateLimitError;
4873
+ exports.isItdValidationError = isItdValidationError;
4874
+ exports.isKnownNotificationType = isKnownNotificationType;
4875
+ exports.isMyProfile = isMyProfile;
4876
+ exports.normalizeNotification = normalizeNotification;
4877
+ exports.poll = poll;
4878
+ exports.post = post;
4879
+ exports.readNotificationEvent = readNotificationEvent;
4880
+ exports.readUnreadCountEvent = readUnreadCountEvent;
4881
+ exports.report = report;
4882
+ exports.resolveNotificationUrl = resolveNotificationUrl;
4883
+ exports.toDate = toDate;
4884
+ //# sourceMappingURL=chunk-QILCVTJI.cjs.map
4885
+ //# sourceMappingURL=chunk-QILCVTJI.cjs.map