itd-sdk-js 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API_REFERENCE.md CHANGED
@@ -30,15 +30,17 @@ import { ITDClient } from 'itd-sdk-js';
30
30
 
31
31
  Для работы автоматического обновления токена создайте файл `.cookies` в корне проекта. Скопируйте содержимое заголовка `Cookie` из любого сетевого запроса к сайту в браузере и вставьте в этот файл.
32
32
 
33
+ **Пути к .env и .cookies:** по умолчанию SDK ищет и сохраняет эти файлы в **корне проекта** (`process.cwd()`). При refresh токена обновлённые значения пишутся в ваш проект, а не в папку пакета. При необходимости можно задать свой корень или явные пути через опции конструктора (см. ниже).
34
+
33
35
  ---
34
36
 
35
37
  ## Авторизация и сессии
36
38
 
37
39
  ### Инициализация клиента
38
40
 
39
- JavaScript
41
+ **Простой вариант (корень проекта = текущая рабочая директория):**
40
42
 
41
- ```
43
+ ```javascript
42
44
  import { ITDClient } from 'itd-sdk-js';
43
45
  import dotenv from 'dotenv';
44
46
 
@@ -49,10 +51,59 @@ client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
49
51
  client.auth.isAuthenticated = true;
50
52
  ```
51
53
 
54
+ **С опциями (projectRoot / envPath / cookiesPath):**
55
+
56
+ Если скрипт запускается из подпапки или нужны явные пути:
57
+
58
+ ```javascript
59
+ const client = new ITDClient({
60
+ baseUrl: 'https://xn--d1ah4a.com',
61
+ userAgent: '...',
62
+ projectRoot: process.cwd(), // корень проекта (по умолчанию process.cwd())
63
+ // либо явные пути:
64
+ // envPath: '/path/to/project/.env',
65
+ // cookiesPath: '/path/to/project/.cookies',
66
+ });
67
+ client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
68
+ ```
69
+
70
+ - `projectRoot` — директория, в которой ищутся `.env` и `.cookies` (по умолчанию `process.cwd()`).
71
+ - `envPath` / `cookiesPath` — при указании переопределяют пути, собранные из `projectRoot`.
72
+
52
73
  ### Автоматическое обновление (Refresh Token)
53
74
 
54
75
  При получении ошибки `401 Unauthorized` клиент автоматически обращается к эндпоинту `/api/v1/auth/refresh`, используя данные из `.cookies`. В случае успеха новый токен сохраняется в `.env`, обновляются куки, и исходный запрос повторяется.
55
76
 
77
+ **Важно:** Для автоматического обновления токена необходим `refresh_token` в файле `.cookies`. Если его нет, SDK не сможет обновить токен автоматически.
78
+
79
+ **Проверка наличия refresh_token:**
80
+ ```javascript
81
+ if (client.hasRefreshToken()) {
82
+ console.log('✅ Refresh token доступен, токен будет обновляться автоматически');
83
+ } else {
84
+ console.log('⚠️ Refresh token не найден - обновите файл .cookies');
85
+ }
86
+ ```
87
+
88
+ **Ручная проверка и обновление токена:**
89
+ ```javascript
90
+ // Проверяет валидность токена и обновляет его при необходимости
91
+ const isValid = await client.validateAndRefreshToken();
92
+ if (isValid) {
93
+ console.log('✅ Токен валиден или успешно обновлен');
94
+ } else {
95
+ console.log('❌ Токен невалиден и не удалось обновить');
96
+ }
97
+ ```
98
+
99
+ **Рекомендация для множественных запросов с интервалами:**
100
+ Если вы делаете запросы с большими интервалами (более 10-15 минут), рекомендуется проверять токен перед каждым запросом:
101
+ ```javascript
102
+ // Перед публикацией поста
103
+ await client.validateAndRefreshToken();
104
+ const post = await client.createPost('Текст поста', 'image.jpg');
105
+ ```
106
+
56
107
  ---
57
108
 
58
109
  ## Методы API: Посты
@@ -137,6 +188,14 @@ client.auth.isAuthenticated = true;
137
188
  - `followUser(username)` **/** `unfollowUser(username)` — подписка/отписка.
138
189
  - `getUserClan(username)` — получение эмодзи-аватара пользователя.
139
190
 
191
+ ## Методы API: Управление токенами
192
+
193
+ - `hasRefreshToken()` — проверяет наличие refresh_token в cookies. Возвращает `boolean`.
194
+ - `validateAndRefreshToken()` — проверяет валидность токена и обновляет его при необходимости. Возвращает `Promise<boolean>`.
195
+ - `refreshAccessToken()` — принудительно обновляет токен через refresh endpoint. Возвращает `Promise<string|null>`.
196
+
197
+ **Рекомендация:** При множественных запросах с интервалами (более 10-15 минут) вызывайте `validateAndRefreshToken()` перед каждым запросом.
198
+
140
199
  ---
141
200
 
142
201
  ## Методы API: Уведомления и поиск
package/README.md CHANGED
@@ -26,11 +26,11 @@ npm install
26
26
 
27
27
  ## Настройка
28
28
 
29
- 1. Создайте `.env` на основе `.env.example` (или используйте переменные окружения).
29
+ 1. Создайте `.env` в корне проекта на основе `.env.example` (или используйте переменные окружения).
30
30
  2. Вставьте свой `ITD_ACCESS_TOKEN` (его можно вытащить из Network в DevTools).
31
- 3. Для работы авто-обновления сессии создайте файл `.cookies` и вставьте туда строку `Cookie` из любого запроса к сайту в браузере.
31
+ 3. Для работы авто-обновления сессии создайте файл `.cookies` в корне проекта и вставьте туда строку `Cookie` из любого запроса к сайту в браузере.
32
32
 
33
- Подробный гайд по настройке лежит в [API_REFERENCE.md](API_REFERENCE.md).
33
+ SDK по умолчанию читает и пишет `.env` и `.cookies` в корне проекта (`process.cwd()`). При обновлении токена изменения сохраняются в ваш проект. При необходимости можно задать `projectRoot` или явные пути в конструкторе — см. [API_REFERENCE.md](API_REFERENCE.md).
34
34
 
35
35
  ## Примеры
36
36
 
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Пример работы с токенами при множественных запросах
3
+ *
4
+ * Демонстрирует правильную обработку истечения токена
5
+ * при публикации нескольких постов с интервалами
6
+ */
7
+
8
+ import { ITDClient } from '../src/client.js';
9
+ import dotenv from 'dotenv';
10
+
11
+ dotenv.config();
12
+
13
+ async function publishMultiplePosts() {
14
+ const client = new ITDClient();
15
+ client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
16
+ client.auth.isAuthenticated = true;
17
+
18
+ console.log('📝 Публикация нескольких постов с проверкой токена\n');
19
+
20
+ // Проверяем наличие refresh_token
21
+ if (!client.hasRefreshToken()) {
22
+ console.error('❌ ВНИМАНИЕ: refresh_token не найден в cookies!');
23
+ console.error('💡 Решение:');
24
+ console.error(' 1. Откройте итд.com в браузере и войдите');
25
+ console.error(' 2. Откройте DevTools (F12) → Network');
26
+ console.error(' 3. Найдите любой запрос к итд.com');
27
+ console.error(' 4. Скопируйте значение заголовка Cookie');
28
+ console.error(' 5. Вставьте в файл .cookies в корне проекта');
29
+ console.error(' 6. Убедитесь, что в Cookie есть refresh_token\n');
30
+ console.error('⚠️ Без refresh_token токен не будет обновляться автоматически!\n');
31
+ } else {
32
+ console.log('✅ Refresh token найден - токен будет обновляться автоматически\n');
33
+ }
34
+
35
+ const posts = [
36
+ { text: 'Первый пост', image: 'image1.jpg' },
37
+ { text: 'Второй пост', image: 'image2.jpg' },
38
+ { text: 'Третий пост', image: 'image3.jpg' }
39
+ ];
40
+
41
+ for (let i = 0; i < posts.length; i++) {
42
+ const post = posts[i];
43
+ console.log(`📝 Публикация поста ${i + 1}/${posts.length}...`);
44
+
45
+ try {
46
+ // ВАЖНО: Проверяем и обновляем токен перед каждым запросом
47
+ // Это особенно важно при больших интервалах между запросами
48
+ const tokenValid = await client.validateAndRefreshToken();
49
+
50
+ if (!tokenValid) {
51
+ console.error(`❌ Токен невалиден и не удалось обновить`);
52
+ console.error(` Пропускаю пост: ${post.text}`);
53
+ continue;
54
+ }
55
+
56
+ // Публикуем пост
57
+ const result = await client.createPost(post.text, post.image);
58
+
59
+ if (result) {
60
+ console.log(`✅ Пост ${i + 1} опубликован: ${result.id}\n`);
61
+ } else {
62
+ console.error(`❌ Не удалось опубликовать пост ${i + 1}\n`);
63
+ }
64
+
65
+ } catch (error) {
66
+ console.error(`❌ Ошибка при публикации поста ${i + 1}: ${error.message}\n`);
67
+ }
68
+
69
+ // Имитация интервала между постами
70
+ if (i < posts.length - 1) {
71
+ console.log('⏳ Ожидание перед следующим постом...\n');
72
+ await new Promise(resolve => setTimeout(resolve, 1000));
73
+ }
74
+ }
75
+
76
+ console.log('✅ Все посты обработаны');
77
+ }
78
+
79
+ async function main() {
80
+ try {
81
+ await publishMultiplePosts();
82
+ } catch (error) {
83
+ console.error('❌ Критическая ошибка:', error.message);
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itd-sdk-js",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Unofficial SDK for итд.com - Node.js library for working with API. Automatic token refresh, session management, and convenient methods for posts, comments, users, and notifications.",
5
5
  "main": "src/client.js",
6
6
  "type": "module",
@@ -15,6 +15,7 @@
15
15
  "examples/basic-usage.js",
16
16
  "examples/quick-start.js",
17
17
  "examples/user-friendly.js",
18
+ "examples/token-validation.js",
18
19
  "examples/README.md",
19
20
  ".env.example",
20
21
  ".cookies.example"
@@ -38,7 +39,7 @@
38
39
  "license": "MIT",
39
40
  "repository": {
40
41
  "type": "git",
41
- "url": "https://github.com/FriceKa/ITD-SDK-js.git"
42
+ "url": "git+https://github.com/FriceKa/ITD-SDK-js.git"
42
43
  },
43
44
  "bugs": {
44
45
  "url": "https://github.com/FriceKa/ITD-SDK-js/issues"
package/src/auth.js CHANGED
@@ -16,6 +16,20 @@ export class AuthManager {
16
16
  this.userData = null;
17
17
  }
18
18
 
19
+ /**
20
+ * Проверяет наличие refresh_token в cookies
21
+ *
22
+ * @returns {boolean} True если refresh_token доступен
23
+ */
24
+ hasRefreshToken() {
25
+ try {
26
+ const cookies = this.client.cookieJar.getCookiesSync(this.client.baseUrl);
27
+ return cookies.some(c => c.key === 'refresh_token');
28
+ } catch (e) {
29
+ return false;
30
+ }
31
+ }
32
+
19
33
  /**
20
34
  * Обновляет accessToken через /api/v1/auth/refresh
21
35
  * ВАЖНО: обычно этот endpoint работает только при наличии refresh-cookie,
@@ -24,6 +38,19 @@ export class AuthManager {
24
38
  * @returns {Promise<string|null>} accessToken или null
25
39
  */
26
40
  async refreshAccessToken() {
41
+ // Проверяем наличие refresh_token перед попыткой обновления
42
+ if (!this.hasRefreshToken()) {
43
+ console.error('❌ Не удалось обновить токен: refresh_token не найден в cookies');
44
+ console.error('💡 Решение:');
45
+ console.error(' 1. Откройте итд.com в браузере и войдите в аккаунт');
46
+ console.error(' 2. Откройте DevTools (F12) → Network');
47
+ console.error(' 3. Найдите любой запрос к итд.com');
48
+ console.error(' 4. Скопируйте значение заголовка Cookie');
49
+ console.error(' 5. Вставьте в файл .cookies в корне проекта');
50
+ console.error(' 6. Убедитесь, что в Cookie есть refresh_token');
51
+ return null;
52
+ }
53
+
27
54
  try {
28
55
  const refreshUrl = `${this.client.baseUrl}/api/v1/auth/refresh`;
29
56
 
@@ -41,8 +68,8 @@ export class AuthManager {
41
68
  this.client.setAccessToken(newToken);
42
69
  this.isAuthenticated = true;
43
70
 
44
- // Сохраняем токен в .env файл
45
- await saveAccessToken(newToken);
71
+ // Сохраняем токен в .env файл (в корне проекта)
72
+ await saveAccessToken(newToken, this.client.envPath);
46
73
 
47
74
  // Обновляем cookies, если они пришли в ответе
48
75
  if (response.headers['set-cookie']) {
@@ -67,7 +94,7 @@ export class AuthManager {
67
94
  );
68
95
  if (importantCookies.length > 0) {
69
96
  const cookieHeader = importantCookies.map(c => `${c.key}=${c.value}`).join('; ');
70
- await saveCookieHeader(cookieHeader);
97
+ await saveCookieHeader(cookieHeader, this.client.cookiesPath);
71
98
  }
72
99
  } catch (e) {
73
100
  console.warn('⚠️ Не удалось сохранить обновленные cookies:', e.message);
@@ -80,7 +107,13 @@ export class AuthManager {
80
107
  return null;
81
108
  } catch (error) {
82
109
  if (error.response) {
83
- console.error('refreshAccessToken failed:', error.response.status, error.response.data);
110
+ const errorData = error.response.data;
111
+ if (errorData?.error?.code === 'REFRESH_TOKEN_MISSING') {
112
+ console.error('❌ Не удалось обновить токен: refresh_token не найден');
113
+ console.error('💡 Решение: обновите файл .cookies из браузера (см. инструкцию выше)');
114
+ } else {
115
+ console.error('refreshAccessToken failed:', error.response.status, error.response.data);
116
+ }
84
117
  } else {
85
118
  console.error('refreshAccessToken failed:', error.message);
86
119
  }
@@ -123,4 +156,36 @@ export class AuthManager {
123
156
  // Реальная проверка происходит на уровне API (401 ошибка)
124
157
  return !!(this.client.accessToken || this.isAuthenticated);
125
158
  }
159
+
160
+ /**
161
+ * Проверяет валидность токена, делая тестовый запрос
162
+ * Если токен истек и есть refresh_token, автоматически обновляет его
163
+ *
164
+ * @returns {Promise<boolean>} True если токен валиден или успешно обновлен
165
+ */
166
+ async validateAndRefreshToken() {
167
+ if (!this.client.accessToken) {
168
+ return false;
169
+ }
170
+
171
+ try {
172
+ // Делаем легкий запрос для проверки токена
173
+ const profileUrl = `${this.client.baseUrl}/api/users/me`;
174
+ const response = await this.client.axios.get(profileUrl);
175
+
176
+ if (response.status === 200) {
177
+ return true; // Токен валиден
178
+ }
179
+
180
+ return false;
181
+ } catch (error) {
182
+ // Если получили 401, пытаемся обновить токен
183
+ if (error.response?.status === 401) {
184
+ const newToken = await this.refreshAccessToken();
185
+ return !!newToken;
186
+ }
187
+
188
+ return false;
189
+ }
190
+ }
126
191
  }
package/src/client.js CHANGED
@@ -5,7 +5,6 @@ import axios from 'axios';
5
5
  import dotenv from 'dotenv';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { fileURLToPath } from 'url';
9
8
  import { CookieJar } from 'tough-cookie';
10
9
  import { wrapper } from 'axios-cookiejar-support';
11
10
  import { AuthManager } from './auth.js';
@@ -20,22 +19,46 @@ import { SearchManager } from './search.js';
20
19
 
21
20
  dotenv.config();
22
21
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = path.dirname(__filename);
25
-
26
22
  export class ITDClient {
27
23
  /**
28
24
  * Инициализация клиента
29
25
  *
30
- * @param {string} baseUrl - Базовый URL сайта (по умолчанию из .env)
31
- * @param {string} userAgent - User-Agent для запросов (по умолчанию из .env)
26
+ * @param {string|Object} baseUrlOrOptions - Базовый URL сайта или объект опций
27
+ * @param {string} [userAgent] - User-Agent (если первый аргумент baseUrl)
28
+ *
29
+ * Опции (если первый аргумент — объект):
30
+ * @param {string} [options.baseUrl] - Базовый URL сайта
31
+ * @param {string} [options.userAgent] - User-Agent
32
+ * @param {string} [options.projectRoot] - Корень проекта (по умолчанию process.cwd()); .env и .cookies ищутся здесь
33
+ * @param {string} [options.envPath] - Полный путь к .env (переопределяет projectRoot для .env)
34
+ * @param {string} [options.cookiesPath] - Полный путь к .cookies (переопределяет projectRoot для .cookies)
32
35
  */
33
- constructor(baseUrl = null, userAgent = null) {
36
+ constructor(baseUrlOrOptions = null, userAgent = null) {
37
+ let baseUrl, projectRoot, envPath, cookiesPath;
38
+
39
+ if (baseUrlOrOptions && typeof baseUrlOrOptions === 'object' && !(baseUrlOrOptions instanceof URL)) {
40
+ const opts = baseUrlOrOptions;
41
+ baseUrl = opts.baseUrl ?? process.env.ITD_BASE_URL ?? 'https://xn--d1ah4a.com';
42
+ userAgent = opts.userAgent ?? process.env.ITD_USER_AGENT ?? null;
43
+ projectRoot = opts.projectRoot ?? process.cwd();
44
+ envPath = opts.envPath ?? path.join(projectRoot, '.env');
45
+ cookiesPath = opts.cookiesPath ?? path.join(projectRoot, '.cookies');
46
+ } else {
47
+ projectRoot = process.cwd();
48
+ baseUrl = baseUrlOrOptions || process.env.ITD_BASE_URL || 'https://xn--d1ah4a.com';
49
+ envPath = path.join(projectRoot, '.env');
50
+ cookiesPath = path.join(projectRoot, '.cookies');
51
+ }
52
+
34
53
  // Используем реальный домен (IDN: итд.com = xn--d1ah4a.com)
35
- this.baseUrl = baseUrl || process.env.ITD_BASE_URL || 'https://xn--d1ah4a.com';
36
- this.userAgent = userAgent || process.env.ITD_USER_AGENT ||
54
+ this.baseUrl = baseUrl;
55
+ this.userAgent = userAgent || process.env.ITD_USER_AGENT ||
37
56
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
38
57
 
58
+ /** Пути к .env и .cookies (корень проекта по умолчанию) */
59
+ this.envPath = envPath;
60
+ this.cookiesPath = cookiesPath;
61
+
39
62
  /** @type {string|null} */
40
63
  this.accessToken = null;
41
64
 
@@ -178,13 +201,12 @@ export class ITDClient {
178
201
  */
179
202
  _loadCookiesFromFile() {
180
203
  try {
181
- const cookiesPath = path.join(__dirname, '..', '.cookies');
182
- if (!fs.existsSync(cookiesPath)) {
204
+ if (!fs.existsSync(this.cookiesPath)) {
183
205
  // Файл не существует - это нормально, просто пропускаем
184
206
  return;
185
207
  }
186
208
 
187
- const cookieHeader = fs.readFileSync(cookiesPath, 'utf8').trim();
209
+ const cookieHeader = fs.readFileSync(this.cookiesPath, 'utf8').trim();
188
210
  if (!cookieHeader) {
189
211
  return;
190
212
  }
@@ -219,6 +241,25 @@ export class ITDClient {
219
241
  return await this.auth.refreshAccessToken();
220
242
  }
221
243
 
244
+ /**
245
+ * Проверяет наличие refresh_token в cookies
246
+ *
247
+ * @returns {boolean} True если refresh_token доступен для обновления токена
248
+ */
249
+ hasRefreshToken() {
250
+ return this.auth.hasRefreshToken();
251
+ }
252
+
253
+ /**
254
+ * Проверяет валидность токена и обновляет его при необходимости
255
+ * Полезно вызывать перед множественными запросами с большими интервалами
256
+ *
257
+ * @returns {Promise<boolean>} True если токен валиден или успешно обновлен
258
+ */
259
+ async validateAndRefreshToken() {
260
+ return await this.auth.validateAndRefreshToken();
261
+ }
262
+
222
263
  /**
223
264
  * Выход из аккаунта
224
265
  *
@@ -3,27 +3,24 @@
3
3
  */
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
6
 
11
7
  /**
12
8
  * Обновляет ITD_ACCESS_TOKEN в .env файле
13
- *
9
+ *
14
10
  * @param {string} newToken - Новый access token
11
+ * @param {string} [envPath] - Путь к .env (по умолчанию process.cwd() + '/.env')
15
12
  * @returns {Promise<boolean>} True если успешно
16
13
  */
17
- export async function saveAccessToken(newToken) {
14
+ export async function saveAccessToken(newToken, envPath = null) {
18
15
  try {
19
- const envPath = path.join(__dirname, '..', '.env');
20
-
21
- if (!fs.existsSync(envPath)) {
16
+ const targetPath = envPath ?? path.join(process.cwd(), '.env');
17
+
18
+ if (!fs.existsSync(targetPath)) {
22
19
  console.warn('⚠️ Файл .env не найден, токен не сохранен');
23
20
  return false;
24
21
  }
25
-
26
- let content = fs.readFileSync(envPath, 'utf8');
22
+
23
+ let content = fs.readFileSync(targetPath, 'utf8');
27
24
 
28
25
  // Ищем строку с ITD_ACCESS_TOKEN
29
26
  const tokenRegex = /^ITD_ACCESS_TOKEN=.*$/m;
@@ -36,7 +33,7 @@ export async function saveAccessToken(newToken) {
36
33
  content += `\nITD_ACCESS_TOKEN=${newToken}\n`;
37
34
  }
38
35
 
39
- fs.writeFileSync(envPath, content, 'utf8');
36
+ fs.writeFileSync(targetPath, content, 'utf8');
40
37
  console.log('✅ Токен сохранен в .env');
41
38
  return true;
42
39
  } catch (error) {
@@ -47,16 +44,17 @@ export async function saveAccessToken(newToken) {
47
44
 
48
45
  /**
49
46
  * Сохраняет cookies в файл .cookies
50
- *
47
+ *
51
48
  * @param {string} newCookieHeader - Новый cookie header
49
+ * @param {string} [cookiesPath] - Путь к .cookies (по умолчанию process.cwd() + '/.cookies')
52
50
  * @returns {Promise<boolean>} True если успешно
53
51
  */
54
- export async function saveCookieHeader(newCookieHeader) {
52
+ export async function saveCookieHeader(newCookieHeader, cookiesPath = null) {
55
53
  try {
56
- const cookiesPath = path.join(__dirname, '..', '.cookies');
57
-
54
+ const targetPath = cookiesPath ?? path.join(process.cwd(), '.cookies');
55
+
58
56
  // Просто записываем cookies в файл (одна строка)
59
- fs.writeFileSync(cookiesPath, newCookieHeader, 'utf8');
57
+ fs.writeFileSync(targetPath, newCookieHeader, 'utf8');
60
58
  console.log('✅ Cookies сохранены в .cookies');
61
59
  return true;
62
60
  } catch (error) {