itd-api 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +90 -252
  2. package/dist/{chunk-HTF2MOM4.cjs → chunk-QD4UHJFF.cjs} +4106 -2730
  3. package/dist/chunk-QD4UHJFF.cjs.map +1 -0
  4. package/dist/{chunk-MYAU2WJU.js → chunk-TB7HW3VX.js} +4097 -2731
  5. package/dist/chunk-TB7HW3VX.js.map +1 -0
  6. package/dist/index-CrlTO7sR.d.cts +4858 -0
  7. package/dist/index-CrlTO7sR.d.ts +4858 -0
  8. package/dist/index.cjs +136 -100
  9. package/dist/index.d.cts +1 -4206
  10. package/dist/index.d.ts +1 -4206
  11. package/dist/index.js +1 -1
  12. package/dist/node.cjs +226 -132
  13. package/dist/node.cjs.map +1 -1
  14. package/dist/node.d.cts +42 -4
  15. package/dist/node.d.ts +42 -4
  16. package/dist/node.js +99 -36
  17. package/dist/node.js.map +1 -1
  18. package/guides/README.md +22 -0
  19. package/guides/authentication/README.md +176 -0
  20. package/guides/authentication/examples/bot-with-session.mjs +98 -0
  21. package/guides/authentication/examples/turnstile-login.mjs +56 -0
  22. package/guides/integrations/README.md +62 -0
  23. package/guides/integrations/examples/proxy.mjs +26 -0
  24. package/guides/multi-accounts/README.md +142 -0
  25. package/guides/multi-accounts/examples/multi-accounts.mjs +71 -0
  26. package/guides/plugins/README.md +162 -0
  27. package/guides/plugins/examples/cache.mjs +33 -0
  28. package/guides/plugins/examples/crypto.mjs +54 -0
  29. package/guides/quickstart/README.md +124 -0
  30. package/guides/quickstart/examples/quick-start.mjs +44 -0
  31. package/guides/quickstart/examples/typescript.ts +90 -0
  32. package/guides/realtime/README.md +109 -0
  33. package/guides/realtime/examples/notifications.mjs +62 -0
  34. package/guides/text-markup/README.md +214 -0
  35. package/guides/text-markup/examples/create-post.mjs +64 -0
  36. package/package.json +6 -4
  37. package/dist/chunk-HTF2MOM4.cjs.map +0 -1
  38. package/dist/chunk-MYAU2WJU.js.map +0 -1
package/README.md CHANGED
@@ -43,65 +43,24 @@ const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json')
43
43
  await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
44
44
  ```
45
45
 
46
- Готовые примеры — в папке [`examples/`](./examples/README.md).
46
+ Продолжение с запускаемыми примерами — в [руководстве по быстрому старту](./guides/quickstart/README.md).
47
+ Все тематические материалы собраны в [`guides/`](./guides/README.md).
47
48
 
48
49
  ---
49
50
 
50
51
  ## Авторизация
51
52
 
52
- Откуда клиент берёт доступ к API — либо из опции `auth`, либо из `storage`, либо
53
- из явного вызова входа. Всё это взаимозаменяемо, и **обязательного варианта нет**.
53
+ Клиент может взять доступ из `auth`, сохранённой сессии или явного вызова `itd.auth`:
54
54
 
55
55
  ```ts
56
- new ItdClient({ auth: '<accessToken>' }); // разовый вызов
57
- new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию строками
58
- new ItdClient({ auth: { email, password, getTurnstileToken } }); // войти самому
59
- new ItdClient({ auth: { getToken: () => vault.read() } }); // токен извне
60
-
61
- new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') }); // сессия с прошлого раза
62
- new ItdClient(); // войти позже, через itd.auth
63
- ```
64
-
65
- `auth` и `storage` не конкурируют, а дополняют друг друга: **хранилище главнее** — оно
66
- отражает текущее состояние сессии, — а недостающие поля берутся из `auth`. Типичный случай:
67
- в хранилище лежит только `accessToken`, а `refreshToken` приходит из настроек приложения.
68
-
69
- ### Сессия из хранилища — `auth` не нужен
70
-
71
- Если предыдущий запуск сохранил сессию, для работы достаточно одного `storage`:
72
-
73
- ```ts
74
- import { ItdClient, FileTokenStorage } from 'itd-api/node';
75
-
76
- const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });
77
-
78
- const me = await itd.users.me(); // токен подставится сам
79
- ```
80
-
81
- Более того, сохранённого `accessToken` не требуется вовсе. Хватает cookie: первый запрос
82
- получит `401`, библиотека продлит сессию и повторит его — вызывающий код ничего не заметит.
83
-
84
- Refresh-токен живёт 30 суток и обновляется при каждом продлении, поэтому регулярно
85
- работающему боту капчу достаточно решить один раз, при первом запуске.
86
-
87
- Проверить, есть ли что продлевать, можно заранее:
88
-
89
- ```ts
90
- if (await itd.auth.hasRefreshSession()) await itd.auth.refresh();
91
- else redirectToLogin();
56
+ new ItdClient({ auth: '<accessToken>' });
57
+ new ItdClient({ auth: { accessToken, refreshToken } });
58
+ new ItdClient({ auth: { email, password, getTurnstileToken } });
59
+ new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });
92
60
  ```
93
61
 
94
- ### Продление токена
95
-
96
- При ответе `401` библиотека продлевает сессию и повторяет запрос. Параллельные запросы,
97
- одновременно получившие `401`, ждут **одного** обновления, а не запускают своё — иначе сервер
98
- увидел бы десяток одновременных `refresh` и отверг бы все, кроме первого.
99
-
100
- Отключить автоматику: `autoRefresh: false`, дальше `await itd.auth.refresh()` вручную.
101
-
102
- Когда продлить не удалось, `refresh()` бросает ошибку **сервера** — по её коду видно, что
103
- именно случилось: `SESSION_NOT_FOUND` (сессия отозвана или истекла), `SESSION_REVOKED`,
104
- `REFRESH_TOKEN_MISSING` (продлевать нечем). Та же ошибка приходит в событии `authError`:
62
+ При `401` сессия продлевается, исходный запрос повторяется, а новое состояние сохраняется.
63
+ Параллельные запросы ждут одного refresh. Потерю сессии можно отследить:
105
64
 
106
65
  ```ts
107
66
  itd.on('authError', ({ error }) => {
@@ -109,108 +68,39 @@ itd.on('authError', ({ error }) => {
109
68
  });
110
69
  ```
111
70
 
112
- ### Капча обязательна при входе
113
-
114
- `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Сам клиент капчу
115
- не решает — он принимает готовый токен, а решает его кто-то снаружи.
116
-
117
- ```ts
118
- import { TURNSTILE_SITE_KEY } from 'itd-api';
119
-
120
- // в браузере — виджет Turnstile с этим ключом
121
- turnstile.render('#captcha', {
122
- sitekey: TURNSTILE_SITE_KEY,
123
- callback: (turnstileToken) => itd.auth.signIn({ email, password, turnstileToken }),
124
- });
125
- ```
126
-
127
- Токен одноразовый и живёт несколько минут. Долгоживущему боту передавайте не строку,
128
- а источник — он спрашивается заново перед каждой попыткой входа:
129
-
130
- ```ts
131
- new ItdClient({
132
- auth: { email, password, getTurnstileToken: () => captchaSolver.solve() },
133
- });
134
- ```
135
-
136
- В Node таким источником может быть [`@itd-api/turnstile`](./turnstile/README.md) — отдельный пакет,
137
- который поднимает браузер и приносит токен. Отдельный он намеренно: тянет за собой Playwright
138
- и требует графической оболочки, а нужен далеко не всем — с сохранённой сессией до входа
139
- по паролю дело обычно вообще не доходит.
140
-
141
- ```sh
142
- npm i @itd-api/turnstile playwright
143
- ```
71
+ Вход, регистрация и восстановление пароля требуют одноразовый Turnstile token. Подробности
72
+ о cookie, `deviceId`, OTP, собственном storage и автоматическом решателе:
73
+ [руководство по авторизации](./guides/authentication/README.md).
144
74
 
145
- ```ts
146
- import { createTurnstileSolver } from '@itd-api/turnstile';
75
+ ---
147
76
 
148
- new ItdClient({
149
- storage: new FileTokenStorage('./.itd-session.json'),
150
- auth: { email, password, getTurnstileToken: createTurnstileSolver() },
151
- });
152
- ```
77
+ ## Несколько аккаунтов
153
78
 
154
- ### Вход с кодом из письма
79
+ `ItdAccounts` хранит именованные клиенты с отдельными tokens, cookie и `deviceId`, но одним
80
+ `MultiTokenStorage`:
155
81
 
156
82
  ```ts
157
- import { createInterface } from 'node:readline/promises';
158
-
159
- const rl = createInterface({ input: process.stdin, output: process.stdout });
83
+ import { ItdAccounts, FileMultiTokenStorage } from 'itd-api/node';
160
84
 
161
- await itd.auth.signInWithOtp({
162
- email, password, turnstileToken,
163
- getOtp: () => rl.question('Код из письма: '),
85
+ const accounts = new ItdAccounts({
86
+ storage: new FileMultiTokenStorage('./.itd-sessions.json'),
87
+ rateLimit: { concurrency: 4 },
164
88
  });
165
- ```
166
89
 
167
- ### Сброс пароля
90
+ // Поднимаем тех, кто уже входил раньше: ни auth, ни капча не нужны.
91
+ await accounts.restore();
168
92
 
169
- Идёт тем же потоком с кодом: `forgotPassword` возвращает `flowToken`, письмо приносит код.
93
+ if (!accounts.has('kiow')) {
94
+ accounts.addAccount('kiow', { auth: { email, password, getTurnstileToken } });
95
+ }
170
96
 
171
- ```ts
172
- await itd.auth.resetPasswordWithOtp({
173
- email, turnstileToken, newPassword,
174
- getOtp: () => rl.question('Код из письма: '),
175
- });
97
+ const itd = accounts.account('kiow'); // обычный ItdClient со всеми разделами
98
+ await itd.posts.create({ content: 'привет' });
99
+ await accounts.close();
176
100
  ```
177
101
 
178
- ### Хранение сессии
179
-
180
- | Хранилище | Откуда | Среда |
181
- |---|---|---|
182
- | `MemoryTokenStorage` (по умолчанию) | `itd-api` | везде |
183
- | `LocalStorageTokenStorage` | `itd-api` | браузер |
184
- | `FileTokenStorage` | `itd-api/node` | Node, Bun, Deno |
185
- | `createTokenStorage({ get, set, clear })` | `itd-api` | своё: Redis, БД, AsyncStorage |
186
-
187
- По умолчанию сессия живёт в памяти процесса и теряется при перезапуске. Укажите `storage` —
188
- и библиотека сама запишет туда всё нужное после входа и после каждого продления; отдельно
189
- сохранять ничего не надо.
190
-
191
- В сессию попадают `accessToken`, `refreshToken`, cookie и `deviceId`. Сохранять её целиком
192
- важно: refresh-токен приходит в cookie `refresh_token`, а `fetch` вне браузера их не хранит,
193
- поэтому библиотека ведёт собственный cookie-jar. В браузере используется
194
- `credentials: 'include'`, в React Native cookie ведёт нативный слой.
195
-
196
- `deviceId` — идентификатор устройства из заголовка `X-Device-Id`. Сервер различает по нему
197
- записи в списке сессий (`itd.auth.sessions()`), поэтому при постоянном хранилище он переживает
198
- перезапуск и бот не плодит по новой сессии на каждый старт. Своё значение — опцией `deviceId`.
199
-
200
- Refresh-токен можно передать и строкой (`auth: { accessToken, refreshToken }`) — вне браузера
201
- библиотека сама подставит его нужной cookie. В браузере так не выйдет: cookie помечена
202
- `HttpOnly`, и из JS её не выставить.
203
-
204
- **Сервер выдаёт при каждом продлении новый refresh-токен взамен прежнего.** Со штатным
205
- `storage` это происходит само. Если же вы храните сессию сами, снимайте её после каждого
206
- обновления, а не один раз при входе, — иначе сохранённое значение протухнет:
207
-
208
- ```ts
209
- itd.on('tokens', async () => saveSomewhere(await itd.getSession()));
210
-
211
- // при следующем запуске
212
- await itd.setSession(await loadFromSomewhere());
213
- ```
102
+ Личные прокси, общая или раздельные очереди, собственное хранилище и события контейнера
103
+ описаны в [руководстве по нескольким аккаунтам](./guides/multi-accounts/README.md).
214
104
 
215
105
  ---
216
106
 
@@ -287,25 +177,48 @@ await itd.posts.create(draft.content('второй')); // заготовка
287
177
  Файлы из `attach()` загружаются автоматически, порядок вложений сохраняется, MIME-тип
288
178
  проверяется до отправки.
289
179
 
290
- Разметка текста передаётся полем `spans` — библиотека её не генерирует и не пересчитывает.
291
- Известные типы собраны в `SpanType`: `hashtag`, `mention`, `link`, `bold`, `italic`,
292
- `underline`, `strike`, `spoiler`, `monospace`, `quote`.
180
+ ### Разметка текста
181
+
182
+ Билдер собирает текст и сам считает смещения:
293
183
 
294
184
  ```ts
295
- import { SpanType } from 'itd-api';
296
-
297
- await itd.posts.create({
298
- content: 'жирное слово и ссылка',
299
- spans: [
300
- { type: SpanType.Bold, offset: 0, length: 6 },
301
- { type: SpanType.Link, offset: 15, length: 6, url: 'https://example.com' },
302
- ],
303
- });
185
+ import { post, renderSpans } from 'itd-api';
186
+
187
+ const created = await itd.posts.create(
188
+ post().markup((m) =>
189
+ m
190
+ .text('смотрите ')
191
+ .hashtag('котики')
192
+ .text(' от ')
193
+ .mention('durov')
194
+ .text(': ')
195
+ .bold('важно'),
196
+ ),
197
+ );
198
+
199
+ renderSpans(created.content, created.spans); // безопасный HTML по умолчанию
200
+ ```
201
+
202
+ Доступны `bold`, `italic`, `underline`, `strike`, `spoiler`, `monospace`, `quote`, `link`,
203
+ `hashtag`, `mention`, `span()` и несколько стилей сразу через `styled()`. Вложенные и
204
+ пересекающиеся spans поддерживаются. Смещения измеряются в единицах UTF-16, как индексы
205
+ строк и DOM Selection в JavaScript.
206
+
207
+ Для обычного текста есть автоматическое обнаружение ссылок, хэштегов и упоминаний:
208
+
209
+ ```ts
210
+ await itd.posts.create(post('#котики от @durov: https://example.com').autoSpans());
304
211
  ```
305
212
 
306
- У `link` адрес лежит в `url`, у `hashtag` и `mention` имя в `tag`.
213
+ `renderSpans()` также выводит Markdown и ANSI и позволяет настроить маршруты упоминаний,
214
+ хэштегов и префикс CSS-классов. Вызов `postBuilder.content(newText)` сбрасывает прежние
215
+ spans, поскольку они рассчитаны для другого текста. `posts.update()` принимает тот же билдер,
216
+ но требует явно заданный `content`.
217
+
218
+ Авторазметка, пересекающиеся стили, обновление и настройка рендера подробно разобраны
219
+ в [руководстве по разметке](./guides/text-markup/README.md).
307
220
 
308
- Билдеры есть у поста, комментария, опроса и жалобы. Все они неизменяемые, а `build()`
221
+ Билдеры есть у разметки, поста, комментария, опроса и жалобы. Все они неизменяемые, а `build()`
309
222
  проверяет данные и бросает `ItdConfigError` **до** обращения к сети:
310
223
 
311
224
  ```ts
@@ -328,33 +241,13 @@ stream.on('notification', ({ notification }) => {
328
241
  console.log(formatNotificationText(notification)); // «Аня и ещё 2 оценили ваш пост»
329
242
  console.log(resolveNotificationUrl(notification)); // '/@anya/post/9f1c…'
330
243
  });
331
- stream.on('unreadCount', (count) => setBadge(count));
332
244
 
333
245
  await stream.connect();
334
246
  ```
335
247
 
336
- События приходят почти мгновенно реакция, комментарий, подписка и репост долетают
337
- за доли секунды после действия.
338
-
339
- Уведомления из потока и из `itd.notifications.list()` приведены к общей форме, поэтому
340
- складываются в один список. Сервер называет типы коротко (`like`, `comment`, `repost`),
341
- библиотека приводит их к однозначным (`post_reaction`, `post_comment`, `post_repost`),
342
- а пришедшее значение оставляет в `rawType`; весь исходный объект — в `raw`.
343
-
344
- `resolveNotificationUrl()` учитывает, что смысл полей зависит от типа: у комментария
345
- цель — пост, а предмет — сам комментарий; у репоста наоборот, цель — репост, а предмет —
346
- исходная запись. Поэтому ссылка на комментарий ведёт на пост с якорем, а на репост —
347
- на сам репост.
348
-
349
- Соединение держится само: обрывы, продление токена и повторные попытки
350
- (`[1, 2, 4, 8, 16, 30] с`, джиттер ±30%, 15 попыток подряд) обрабатываются внутри.
351
- Сервер шлёт keep-alive `: ping` каждые 15 секунд; если тишина длится дольше `idleTimeout`
352
- (90 секунд по умолчанию), соединение считается мёртвым и поднимается заново.
353
- В браузере поток дополнительно переподключается при возврате вкладки из фона
354
- и восстановлении сети.
355
-
356
- > Счётчик непрочитанных сервер по потоку **не присылает** — событие `unreadCount`
357
- > на практике не срабатывает. Считайте сами либо запрашивайте `itd.notifications.count()`.
248
+ REST и поток используют одну форму уведомления. Переподключение, refresh token, keep-alive
249
+ и fallback на polling обрабатываются внутри. Эксплуатационные настройки и счётчик
250
+ непрочитанных разобраны в [руководстве по realtime](./guides/realtime/README.md).
358
251
 
359
252
  ---
360
253
 
@@ -407,6 +300,11 @@ await itd.request({ method: 'GET', service: 'pb', path: '/api/pixel-info', query
407
300
  То же самое после создания клиента — `itd.defineService({ name, baseUrl, headers, auth })`;
408
301
  базовый URL сервиса отдаёт `itd.serviceBaseUrl(name)`.
409
302
 
303
+ Bearer-токен по умолчанию отправляется только основному хосту и его поддоменам.
304
+ Публичный или сторонний сервис его не получает. То же правило действует для разового
305
+ `itd.request({ baseUrl })`; если внешнему хосту действительно нужна авторизация,
306
+ разрешите её явно через `skipAuth: false`.
307
+
410
308
  У каждого сервиса своя очередь `rateLimit`: лимит частоты сервер считает по хосту, поэтому
411
309
  `429` от статуса не тормозит основной API и наоборот.
412
310
 
@@ -538,93 +436,32 @@ await fetch.close(); // закрывает пул соединений
538
436
  ```
539
437
 
540
438
  Через тот же `fetch` пойдут авторизация, cookie, очередь, повторы и поток уведомлений.
541
- Только для Node/Bun/Deno. Подробности в [README пакета](./proxy/README.md).
439
+ Только для Node/Bun/Deno. Подключение proxy и Turnstile разобрано в
440
+ [руководстве по интеграциям](./guides/integrations/README.md), параметры транспорта — в
441
+ [README пакета](./proxy/README.md).
542
442
 
543
443
  ---
544
444
 
545
445
  ## Плагины
546
446
 
547
- Плагин обёртка вокруг запроса: она видит тело до отправки и разобранный ответ, поэтому
548
- одна обёртка охватывает сразу все методы клиента. Подключается через `itd.use()`:
447
+ Плагин оборачивает запросы и ответы сразу всех ресурсов:
549
448
 
550
449
  ```ts
551
450
  import { ItdClient } from 'itd-api';
552
- import { crypt } from '@itd-api/crypto';
451
+ import { cache } from '@itd-api/cache';
553
452
 
554
453
  const itd = new ItdClient({ auth: token });
555
- itd.use(crypt());
556
- ```
557
-
558
- ### `@itd-api/crypto` скрытые сообщения
559
-
560
- [Отдельный пакет](./crypto/README.md): прячет текст в невидимых символах внутри обычного поста.
561
- Читатель видит обложку, а тот, у кого подключён плагин, получает спрятанное отдельным полем.
562
-
563
- ```sh
564
- npm i @itd-api/crypto
565
- ```
566
-
567
- ```ts
568
- // отправка: текст прогоняется через шифр, обложка остаётся видимой
569
- const created = await itd.posts.create(
570
- { content: 'секретный текст' },
571
- { encrypt: { cipher: 'invisible', cover: 'обычный пост' } },
454
+ itd.use(
455
+ cache({
456
+ ttl: 60_000,
457
+ routes: ['users.get', 'posts.get', 'posts.list'],
458
+ }),
572
459
  );
573
-
574
- // чтение: content не меняется, расшифровка приезжает рядом
575
- const post = await itd.posts.get(created.id);
576
- post.secret?.text; // 'секретный текст'
577
- ```
578
-
579
- Работает для постов, комментариев, ответов, имени и подписи профиля. Расшифровка идёт сама
580
- и вглубь: находки появляются и у постов ленты, и у исходного поста репоста, и у авторов.
581
-
582
- Шифра два: `invisible` — невидимые символы с обложкой, `beecrypt` — видимый текст из букв
583
- `жъЖЪ`. Подробности, ограничения и то, как подключить свой шифр, — в
584
- [README пакета](./crypto/README.md).
585
-
586
- ### Свой плагин
587
-
588
- ```ts
589
- import type { ItdPlugin } from 'itd-api';
590
-
591
- const timing: ItdPlugin = {
592
- name: 'timing',
593
- install({ use, logger }) {
594
- use(async (request, next) => {
595
- const started = Date.now();
596
- try {
597
- return await next(request);
598
- } finally {
599
- logger?.info(`${request.method} ${request.path}: ${Date.now() - started} мс`);
600
- }
601
- });
602
- },
603
- };
604
- ```
605
-
606
- Обёртка может изменить запрос (передайте в `next` копию), подменить ответ или вернуть своё,
607
- не обращаясь к сети. Подключённая раньше оказывается снаружи. Выполняется она один раз
608
- на запрос, независимо от числа повторов.
609
-
610
- Свои опции запроса плагин объявляет сам — библиотека их не понимает, но доносит до обёртки
611
- нетронутыми:
612
-
613
- ```ts
614
- const plugin: ItdPlugin = {
615
- name: 'мой',
616
- optionKeys: ['мояОпция'],
617
- install({ use }) { /* … */ },
618
- };
619
-
620
- declare module 'itd-api' {
621
- interface RequestOptions { мояОпция?: string | undefined }
622
- }
623
460
  ```
624
461
 
625
- Имена полей самого запроса (`path`, `body`, `headers`, `signal` и прочие) заявить нельзя:
626
- подключение такого плагина завершится `ItdConfigError`. Иначе опечатка в `optionKeys`
627
- молча подменяла бы путь или тело любого вызова.
462
+ Официальные плагины Cache и Crypto, полный контракт `ItdPlugin`, собственные опции и
463
+ структура пакета описаны в
464
+ [руководстве по плагинам](./guides/plugins/README.md).
628
465
 
629
466
  ---
630
467
 
@@ -644,6 +481,7 @@ declare module 'itd-api' {
644
481
  | `itd.realtime()` | поток уведомлений |
645
482
  | `itd.use()` | плагины: обёртки вокруг запроса и ответа |
646
483
  | `itd.request()` | произвольный запрос, если метода ещё нет |
484
+ | `ItdAccounts` | несколько аккаунтов с общим хранилищем сессий |
647
485
 
648
486
  Метода не хватает или ответ разошёлся с документацией — есть запасной путь:
649
487
 
@@ -671,7 +509,7 @@ TypeScript 5.0+. Пакет собран в ESM и CommonJS, типы корре
671
509
 
672
510
  ```bash
673
511
  npm install
674
- npm test # 417 тестов
512
+ npm test # 611 тестов
675
513
  npm run test:all # вместе с пакетами workspace
676
514
  npm run typecheck
677
515
  npm run lint