itd-api 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,10 +36,9 @@ for await (const post of itd.posts.iterate({ tab: FeedTab.Following })) {
36
36
  ```ts
37
37
  import { ItdClient, FileTokenStorage } from 'itd-api/node';
38
38
 
39
- const itd = new ItdClient({
40
- auth: { email, password, getTurnstileToken }, // вход требует капчи, см. ниже
41
- storage: new FileTokenStorage('./.itd-session.json'),
42
- });
39
+ // Сессия из файла: продлевается сама, `auth` не нужен. Как её туда положить —
40
+ // в разделе «Авторизация»: вход требует капчи, поэтому делается один раз.
41
+ const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });
43
42
 
44
43
  await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
45
44
  ```
@@ -50,21 +49,66 @@ await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
50
49
 
51
50
  ## Авторизация
52
51
 
53
- Четыре формы на выбор:
52
+ Откуда клиент берёт доступ к API — либо из опции `auth`, либо из `storage`, либо
53
+ из явного вызова входа. Всё это взаимозаменяемо, и **обязательного варианта нет**.
54
54
 
55
55
  ```ts
56
56
  new ItdClient({ auth: '<accessToken>' }); // разовый вызов
57
- new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию
57
+ new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию строками
58
58
  new ItdClient({ auth: { email, password, getTurnstileToken } }); // войти самому
59
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(); // токен подставится сам
60
79
  ```
61
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();
92
+ ```
93
+
94
+ ### Продление токена
95
+
62
96
  При ответе `401` библиотека продлевает сессию и повторяет запрос. Параллельные запросы,
63
97
  одновременно получившие `401`, ждут **одного** обновления, а не запускают своё — иначе сервер
64
98
  увидел бы десяток одновременных `refresh` и отверг бы все, кроме первого.
65
99
 
66
100
  Отключить автоматику: `autoRefresh: false`, дальше `await itd.auth.refresh()` вручную.
67
101
 
102
+ Когда продлить не удалось, `refresh()` бросает ошибку **сервера** — по её коду видно, что
103
+ именно случилось: `SESSION_NOT_FOUND` (сессия отозвана или истекла), `SESSION_REVOKED`,
104
+ `REFRESH_TOKEN_MISSING` (продлевать нечем). Та же ошибка приходит в событии `authError`:
105
+
106
+ ```ts
107
+ itd.on('authError', ({ error }) => {
108
+ if (isItdApiError(error)) console.error('Сессия потеряна:', error.code);
109
+ });
110
+ ```
111
+
68
112
  ### Капча обязательна при входе
69
113
 
70
114
  `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Полностью
@@ -123,24 +167,33 @@ await itd.auth.resetPasswordWithOtp({
123
167
  | `FileTokenStorage` | `itd-api/node` | Node, Bun, Deno |
124
168
  | `createTokenStorage({ get, set, clear })` | `itd-api` | своё: Redis, БД, AsyncStorage |
125
169
 
126
- Refresh-токен приходит в cookie `refresh_token`, а `fetch` вне браузера их не хранит
127
- библиотека ведёт собственный cookie-jar и сохраняет его вместе с сессией. В браузере
128
- используется `credentials: 'include'`, в React Native cookie ведёт нативный слой.
170
+ По умолчанию сессия живёт в памяти процесса и теряется при перезапуске. Укажите `storage`
171
+ и библиотека сама запишет туда всё нужное после входа и после каждого продления; отдельно
172
+ сохранять ничего не надо.
173
+
174
+ В сессию попадают `accessToken`, `refreshToken`, cookie и `deviceId`. Сохранять её целиком
175
+ важно: refresh-токен приходит в cookie `refresh_token`, а `fetch` вне браузера их не хранит,
176
+ поэтому библиотека ведёт собственный cookie-jar. В браузере используется
177
+ `credentials: 'include'`, в React Native cookie ведёт нативный слой.
178
+
179
+ `deviceId` — идентификатор устройства из заголовка `X-Device-Id`. Сервер различает по нему
180
+ записи в списке сессий (`itd.auth.sessions()`), поэтому при постоянном хранилище он переживает
181
+ перезапуск и бот не плодит по новой сессии на каждый старт. Своё значение — опцией `deviceId`.
129
182
 
130
- Токен можно передать и строкой (`auth: { accessToken, refreshToken }`) — вне браузера
131
- библиотека сама подставит его нужной cookie. В браузере так не выйдет: настоящая cookie
132
- помечена `HttpOnly`, и из JS её не выставить.
183
+ Refresh-токен можно передать и строкой (`auth: { accessToken, refreshToken }`) — вне браузера
184
+ библиотека сама подставит его нужной cookie. В браузере так не выйдет: cookie помечена
185
+ `HttpOnly`, и из JS её не выставить.
133
186
 
134
- Сервер выдаёт при каждом обновлении **новый** refresh-токен взамен прежнего, поэтому
135
- сохранять сессию нужно после обновления, а не один раз при входе:
187
+ **Сервер выдаёт при каждом продлении новый refresh-токен взамен прежнего.** Со штатным
188
+ `storage` это происходит само. Если же вы храните сессию сами, снимайте её после каждого
189
+ обновления, а не один раз при входе, — иначе сохранённое значение протухнет:
136
190
 
137
191
  ```ts
138
192
  itd.on('tokens', async () => saveSomewhere(await itd.getSession()));
139
- ```
140
193
 
141
- Вместе с сессией хранится `deviceId` — идентификатор устройства из заголовка `X-Device-Id`.
142
- Сервер различает по нему записи в списке сессий, поэтому при постоянном хранилище он
143
- переживает перезапуск, а бот не плодит по новой сессии на каждый старт.
194
+ // при следующем запуске
195
+ await itd.setSession(await loadFromSomewhere());
196
+ ```
144
197
 
145
198
  ---
146
199
 
@@ -283,11 +336,14 @@ try {
283
336
  ```ts
284
337
  const itd = new ItdClient({
285
338
  baseUrl: 'https://xn--d1ah4a.com', // свой прокси, если работаете из браузера
286
- auth: { email, password },
339
+ auth: { email, password, getTurnstileToken },
287
340
  storage: new FileTokenStorage('./.itd-session.json'),
288
341
  timeout: 30_000,
289
342
  retry: { attempts: 3, retryWrites: false },
290
343
  rateLimit: { concurrency: 4, rps: 8 },
344
+ // Заголовки латиницей: кириллица в них запрещена самим HTTP.
345
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36', // по умолчанию itd-api/<версия>; false — не слать
346
+ deviceId: '3f2a…-uuid', // по умолчанию заводится сам и живёт в сессии
291
347
  logger: true, // токены и пароли в логах маскируются
292
348
  hooks: {
293
349
  onRequest: (ctx) => console.log(ctx.method, ctx.path),
@@ -300,6 +356,21 @@ const itd = new ItdClient({
300
356
  по умолчанию не повторяется (`retryWrites: false`): повтор мог бы создать дубль поста.
301
357
  Чтения повторяются с экспоненциальным откатом.
302
358
 
359
+ **Очередь: `concurrency` и `rps` решают разные задачи.** Все запросы идут через одну очередь
360
+ клиента, поэтому достаточно **одного экземпляра `ItdClient` на приложение** — разложите его
361
+ по модулям, и темп будет общим.
362
+
363
+ `concurrency` (по умолчанию 6) ограничивает только одновременность. От ограничения частоты
364
+ он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс, а окно
365
+ сервера — около 5 запросов на минуту с лишним. Темп задаёт `rps`:
366
+
367
+ ```ts
368
+ rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запроса в 2 секунды
369
+ ```
370
+
371
+ Ставить `concurrency: 1` без нужды не стоит: загрузка видео с таймаутом в 300 секунд
372
+ заблокирует на это время вообще всё остальное.
373
+
303
374
  **Ограничение частоты — отдельный механизм.** Сервер разрешает около 5 запросов в окно,
304
375
  не присылает `Retry-After` и не сообщает, когда окно сбросится: есть только заголовки
305
376
  `x-ratelimit-limit` и `x-ratelimit-remaining` (доступны на `ItdRateLimitError` как