@pachca/cli 2026.11.0 → 2026.11.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.11.2 (10 ноября 2026)
4
+
5
+ - **Добавлено** (bots listen-events): События бота в реальном времени без публичного адреса: JSONL в терминале или пересылка на локальный обработчик, как исходящий вебхук, с подписью, свежим `webhook_timestamp` и ответом на отправку формы. Запись удаляется из истории после ответа 2xx.
6
+ - **Изменено**: Для запуска CLI требуется Node.js 20.3.0 или новее
7
+
8
+ ## 2026.11.1 (9 ноября 2026)
9
+
10
+ - **Изменено** (auth status): Показывает токен, с которым работают команды, в том числе из `--token` и `PACHCA_TOKEN`. Владельца, права и срок жизни такого токена команда спрашивает у сервера и называет сохранённый профиль, который не используется, пока задан `PACHCA_TOKEN`.
11
+ - **Изменено**: Подсказки к отказу `insufficient_scope` ведут к `pachca auth status` с тем же `--token` или `--profile`, что у команды, а для сохранённого профиля в `-o json` ещё и с `--remote`
12
+ - **Изменено** (auth login): Подсказка `pachca auth status` после входа называет профиль, если без него команда покажет другой токен, а при заданном `PACHCA_TOKEN` вход предупреждает, что команды используют переменную
13
+ - **Добавлено** (messages get, messages list, messages create, messages update, search list-messages): В ответах с сообщениями есть поля `source` и `application`: через что сотрудник отправил сообщение и какая программа
14
+ - **Изменено** (chats list, messages list): `--order` без `--sort` задаёт направление по `id`
15
+ - **Изменено** (bots create, bots update, bots update-webhook): В описании адреса вебхука коды отказа, а в описании шаблона проверка синтаксиса
16
+ - **Изменено** (users update): В описании чата для перевода в гости письмо о смене роли
17
+
3
18
  ## 2026.11.0 (4 ноября 2026)
4
19
 
5
20
  - **Изменено** (tasks create): Работает без `--kind` и не спрашивает тип в терминале: по умолчанию создаётся простое напоминание
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## Установка
6
6
 
7
+ Требуется Node.js 20.3.0 или новее.
8
+
7
9
  ```
8
10
  npm install -g @pachca/cli
9
11
 
@@ -292,6 +294,24 @@ cat data.csv | pachca upload - --name data.csv
292
294
 
293
295
  Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений. Расширение `name` в файле сообщения должно совпадать с расширением имени в `key`.
294
296
 
297
+ ## События бота
298
+
299
+ ```bash
300
+ PACHCA_TOKEN=$PACHCA_BOT_TOKEN pachca bots listen-events
301
+ PACHCA_TOKEN=$PACHCA_BOT_TOKEN PACHCA_SIGNING_SECRET=$PACHCA_SIGNING_SECRET \
302
+ pachca bots listen-events --forward-to http://localhost:3000/webhook
303
+ ```
304
+
305
+ Нужен токен бота с правом `webhooks:events:read` и включённая настройка «Сохранять историю событий». Команда подключается к потоку событий, дожидается подтверждения подписки, дочитывает историю от старых событий к новым и меняет ссылку до истечения срока.
306
+
307
+ Без `--forward-to` каждая запись истории (`id`, `event_type`, `payload`, `created_at`) выводится строкой JSONL и остаётся в истории. При запуске печатается вся накопленная история, после переподключения только новые записи.
308
+
309
+ С `--forward-to` команда отправляет `payload` запросом POST, как исходящий вебхук, и удаляет запись из истории после ответа 2xx. Для этого нужно право `webhooks:events:delete`. Если задан `PACHCA_SIGNING_SECRET`, тело подписано заголовком `Pachca-Signature`. В `webhook_timestamp` стоит время пересылки, а в `Idempotency-Key` приходит `id` записи. Ответ обработчика на отправку формы (2xx, либо 400 или 422 с `errors`) команда передаёт в Пачку, для этого нужно право `views:write`. Пачка принимает ответ на форму в течение 5 секунд с отправки. Отправку формы старше 10 секунд команда обработчику не пересылает и удаляет из истории: сотрудник к этому времени уже видел ошибку и мог отправить форму снова. Другой ответ, ошибка или таймаут останавливают команду, а запись остаётся в истории. Редиректы не выполняются. Запись, которую обработчик не может принять, удалите тем же токеном бота командой `PACHCA_TOKEN=$PACHCA_BOT_TOKEN pachca bots remove-event <id> --force`: `id` есть в сообщении об ошибке.
310
+
311
+ `--forward-to` рассчитан на бота без адреса вебхука. Запускайте для бота одну такую команду: история у слушателей общая.
312
+
313
+ `--timeout` задаёт таймаут запросов к API и к обработчику и ожидания подтверждения подписки в секундах (по умолчанию `defaults.timeout`, `PACHCA_TIMEOUT` или 30). `--no-retry` отключает переподключение, а отдельные запросы к API по-прежнему повторяются при 429 и 5xx. `--verbose` не печатает HTTP-диагностику этой команды, чтобы подписанная ссылка не попала в логи. Ctrl+C завершает команду.
314
+
295
315
  ## Пагинация
296
316
 
297
317
  ```
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## Установка
6
6
 
7
+ Требуется Node.js 20.3.0 или новее.
8
+
7
9
  ```
8
10
  npm install -g @pachca/cli
9
11
 
@@ -74,6 +76,24 @@ cat data.csv | pachca upload - --name data.csv
74
76
 
75
77
  Команда `upload` автоматически получает подпись (`POST /uploads`) и загружает файл на S3. Возвращает `key` — используйте его в `--files` при создании сообщений. Расширение `name` в файле сообщения должно совпадать с расширением имени в `key`.
76
78
 
79
+ ## События бота
80
+
81
+ ```bash
82
+ PACHCA_TOKEN=$PACHCA_BOT_TOKEN pachca bots listen-events
83
+ PACHCA_TOKEN=$PACHCA_BOT_TOKEN PACHCA_SIGNING_SECRET=$PACHCA_SIGNING_SECRET \
84
+ pachca bots listen-events --forward-to http://localhost:3000/webhook
85
+ ```
86
+
87
+ Нужен токен бота с правом `webhooks:events:read` и включённая настройка «Сохранять историю событий». Команда подключается к потоку событий, дожидается подтверждения подписки, дочитывает историю от старых событий к новым и меняет ссылку до истечения срока.
88
+
89
+ Без `--forward-to` каждая запись истории (`id`, `event_type`, `payload`, `created_at`) выводится строкой JSONL и остаётся в истории. При запуске печатается вся накопленная история, после переподключения только новые записи.
90
+
91
+ С `--forward-to` команда отправляет `payload` запросом POST, как исходящий вебхук, и удаляет запись из истории после ответа 2xx. Для этого нужно право `webhooks:events:delete`. Если задан `PACHCA_SIGNING_SECRET`, тело подписано заголовком `Pachca-Signature`. В `webhook_timestamp` стоит время пересылки, а в `Idempotency-Key` приходит `id` записи. Ответ обработчика на отправку формы (2xx, либо 400 или 422 с `errors`) команда передаёт в Пачку, для этого нужно право `views:write`. Пачка принимает ответ на форму в течение 5 секунд с отправки. Отправку формы старше 10 секунд команда обработчику не пересылает и удаляет из истории: сотрудник к этому времени уже видел ошибку и мог отправить форму снова. Другой ответ, ошибка или таймаут останавливают команду, а запись остаётся в истории. Редиректы не выполняются. Запись, которую обработчик не может принять, удалите тем же токеном бота командой `PACHCA_TOKEN=$PACHCA_BOT_TOKEN pachca bots remove-event <id> --force`: `id` есть в сообщении об ошибке.
92
+
93
+ `--forward-to` рассчитан на бота без адреса вебхука. Запускайте для бота одну такую команду: история у слушателей общая.
94
+
95
+ `--timeout` задаёт таймаут запросов к API и к обработчику и ожидания подтверждения подписки в секундах (по умолчанию `defaults.timeout`, `PACHCA_TIMEOUT` или 30). `--no-retry` отключает переподключение, а отдельные запросы к API по-прежнему повторяются при 429 и 5xx. `--verbose` не печатает HTTP-диагностику этой команды, чтобы подписанная ссылка не попала в логи. Ctrl+C завершает команду.
96
+
77
97
  ## Пагинация
78
98
 
79
99
  ```
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, profileFlag, } from './profiles.js';
2
+ import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, profileFlag, tokenSource, } from './profiles.js';
3
3
  import { ApiError, getExitCode, formatDryRun, request } from './client.js';
4
4
  import { outputData, outputError, outputSuccess } from './output.js';
5
5
  import { defaultOutputFormat, isInteractive } from './utils.js';
@@ -246,16 +246,38 @@ export class BaseCommand extends Command {
246
246
  reportScopeError(err) {
247
247
  const format = this.getOutputFormat();
248
248
  const scope = typeof err.details.scope === 'string' ? err.details.scope : undefined;
249
- if (format === 'json' || !process.stderr.isTTY) {
250
- outputError(err.details, format);
249
+ // `auth status` checks the token every command uses. A bare run reads the active
250
+ // profile; a token or profile named on this command, by a flag or by a variable that
251
+ // may have been set for this one command, has to be named again. The token value is
252
+ // never echoed back.
253
+ const source = tokenSource({ token: this.parsedFlags.token, profile: this.parsedFlags.profile });
254
+ const human = format !== 'json' && process.stderr.isTTY;
255
+ const again = source.kind === 'flag'
256
+ ? human ? ' --token <токен>' : ' --token <token>'
257
+ : source.kind === 'profile' && source.via !== 'active_profile'
258
+ ? ` --profile ${source.profileName}`
259
+ : '';
260
+ // A token from the environment may have been set for this one command
261
+ // (`PACHCA_TOKEN=… pachca …`): a bare `auth status` would then read another one.
262
+ const check = source.kind === 'env'
263
+ ? `PACHCA_TOKEN=${human ? '<токен>' : '<token>'} pachca auth status`
264
+ : 'pachca auth status';
265
+ if (!human) {
266
+ // A saved profile keeps the scope list from login, which may still name the
267
+ // scope just refused: only the server's answer settles it.
268
+ const remote = source.kind === 'profile' ? ' --remote' : '';
269
+ outputError({ ...err.details, hint: `${check}${remote}${again}` }, format);
251
270
  this.exit(3);
252
271
  }
253
272
  const resolved = this.tryResolveProfile();
254
273
  const profile = resolved?.profile;
255
274
  process.stderr.write(`✗ Не хватает права${scope ? ` ${scope}` : ''}.\n`);
275
+ // A command's own check before any request (no HTTP status) says why it needs the right.
276
+ if (err.details.code === null)
277
+ process.stderr.write(` ${err.details.error}.\n`);
256
278
  if (!profile) {
257
279
  process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
258
- process.stderr.write(` Проверить права токена: pachca oauth token-info -o json\n`);
280
+ process.stderr.write(` Проверить токен и его права: ${check}${again}\n`);
259
281
  this.exit(3);
260
282
  }
261
283
  // The scope list in the profile is a copy taken at login. When it still names
@@ -265,7 +287,7 @@ export class BaseCommand extends Command {
265
287
  const listedLocally = !!scope && !!profile.scopes?.includes(scope);
266
288
  if (listedLocally) {
267
289
  process.stderr.write(` В сохранённом списке право есть, но у токена его уже нет: список устарел.\n`);
268
- process.stderr.write(` Обновить список: pachca auth status --remote${flag}\n`);
290
+ process.stderr.write(` Обновить список: pachca auth status --remote${again}\n`);
269
291
  }
270
292
  else {
271
293
  process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
package/dist/client.js CHANGED
@@ -98,14 +98,20 @@ function parseRetryAfter(header) {
98
98
  return null;
99
99
  return Math.max(ms, 0);
100
100
  }
101
- export async function request(opts, clientFlags) {
102
- const url = buildUrl(opts.path, opts.query);
101
+ /**
102
+ * Seconds to wait: the value asked for (a flag or `defaults.timeout`), else PACHCA_TIMEOUT, else 30.
103
+ * A value that is not a positive number means 30: a timeout of 0 would abort on the next tick,
104
+ * which is never what anyone means by `--timeout 0`. Timers take at most 2^31 - 1 ms.
105
+ */
106
+ export function resolveTimeoutSeconds(requested) {
103
107
  const rawEnvTimeout = process.env.PACHCA_TIMEOUT ? Number(process.env.PACHCA_TIMEOUT) : undefined;
104
108
  const envTimeout = rawEnvTimeout && !Number.isNaN(rawEnvTimeout) ? rawEnvTimeout : undefined;
105
- const requestedTimeout = opts.timeout ?? clientFlags?.timeout ?? envTimeout ?? DEFAULT_TIMEOUT;
106
- // A non-positive timeout aborts on the next tick, which is never what anyone
107
- // means by `--timeout 0`. The env var already ignored 0; make the flag agree.
108
- const timeoutSeconds = requestedTimeout > 0 ? requestedTimeout : DEFAULT_TIMEOUT;
109
+ const value = Number(requested ?? envTimeout ?? DEFAULT_TIMEOUT);
110
+ return Number.isFinite(value) && value > 0 ? Math.min(value, 2_147_483) : DEFAULT_TIMEOUT;
111
+ }
112
+ export async function request(opts, clientFlags) {
113
+ const url = buildUrl(opts.path, opts.query);
114
+ const timeoutSeconds = resolveTimeoutSeconds(opts.timeout ?? clientFlags?.timeout);
109
115
  const noRetry = opts.noRetry ?? clientFlags?.['no-retry'] ?? false;
110
116
  const method = opts.method.toUpperCase();
111
117
  // The same set is retried after a 5xx and after a network error. A repeated
@@ -1,9 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
- import { setProfile, setActiveProfile, getActiveProfile, getProfile, getSecretStorage, expiresAtFromNow, } from '../../profiles.js';
3
+ import { setProfile, setActiveProfile, getActiveProfile, getProfile, getSecretStorage, expiresAtFromNow, tokenSource, } from '../../profiles.js';
4
4
  import { isSecretStoreAvailable } from '../../secret-store.js';
5
5
  import { ApiError, request } from '../../client.js';
6
6
  import { outputError } from '../../output.js';
7
+ import { fetchIdentity } from '../../identity.js';
7
8
  import { OAuthError, copyToClipboard, deviceName, openBrowser, pollForToken, requestDeviceCode, } from '../../oauth.js';
8
9
  export default class AuthLogin extends BaseCommand {
9
10
  static description = 'Вход в аккаунт и сохранение профиля';
@@ -68,7 +69,7 @@ export default class AuthLogin extends BaseCommand {
68
69
  ? { access_token: flags.token }
69
70
  : await this.deviceLogin(profileName, !flags['no-browser'], flags.quiet);
70
71
  const tokenInfo = await this.verifyToken(grant.access_token);
71
- const identity = await this.fetchIdentity(grant.access_token, tokenInfo.user_id);
72
+ const identity = await fetchIdentity(grant.access_token, tokenInfo.user_id);
72
73
  const scopes = grant.scope ? grant.scope.split(' ').filter(Boolean) : tokenInfo.scopes;
73
74
  const profile = {
74
75
  type: identity.isBot ? 'bot' : 'user',
@@ -197,22 +198,6 @@ export default class AuthLogin extends BaseCommand {
197
198
  throw error;
198
199
  }
199
200
  }
200
- async fetchIdentity(token, userId) {
201
- try {
202
- const response = await request({ method: 'GET', path: '/profile', token }, { quiet: true });
203
- const data = response.data.data;
204
- const name = `${data.first_name || ''} ${data.last_name || ''}`.trim();
205
- return {
206
- name: name || `User #${userId}`,
207
- email: data.email || null,
208
- isBot: data.bot || false,
209
- };
210
- }
211
- catch {
212
- // profile:read scope not available — user_id from token info is enough
213
- return { name: `User #${userId}`, email: null, isBot: false };
214
- }
215
- }
216
201
  report(profileName, profile, scopes) {
217
202
  const format = this.getOutputFormat();
218
203
  if (format === 'json') {
@@ -243,7 +228,14 @@ export default class AuthLogin extends BaseCommand {
243
228
  else {
244
229
  process.stderr.write(` Токен в файле конфигурации (хранилище ключей недоступно)\n`);
245
230
  }
246
- process.stderr.write(` Состояние входа: pachca auth status\n`);
231
+ // A bare `auth status` shows the token other commands would use: PACHCA_TOKEN
232
+ // when it is set, or the active profile, which need not be the one just saved.
233
+ const src = tokenSource({});
234
+ const statusFlag = src.kind === 'profile' && src.profileName === profileName ? '' : ` --profile ${profileName}`;
235
+ process.stderr.write(` Состояние входа: pachca auth status${statusFlag}\n`);
236
+ if (src.kind === 'env') {
237
+ process.stderr.write(` Пока задан PACHCA_TOKEN, команды используют его, а не профиль ${profileName}\n`);
238
+ }
247
239
  }
248
240
  }
249
241
  /** Read a token piped into the command, so it never appears in the shell history. */
@@ -1,8 +1,9 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
- import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, updateProfileScopes, } from '../../profiles.js';
4
- import { request } from '../../client.js';
3
+ import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, listProfiles, tokenSource, updateProfileScopes, } from '../../profiles.js';
4
+ import { ApiError, request } from '../../client.js';
5
5
  import { outputError } from '../../output.js';
6
+ import { fetchIdentity } from '../../identity.js';
6
7
  /** Human-readable time left, or null when the token has no expiry at all. */
7
8
  function describeExpiry(profile) {
8
9
  if (getAuthMethod(profile) === 'token')
@@ -18,10 +19,11 @@ function describeExpiry(profile) {
18
19
  return minutes < 60 ? `осталось ${minutes} мин` : `осталось ${Math.round(minutes / 60)} ч`;
19
20
  }
20
21
  export default class AuthStatus extends BaseCommand {
21
- static description = 'Статус текущего профиля';
22
+ static description = 'Чей токен используют команды и какие у него права';
22
23
  static examples = [
23
24
  '<%= config.bin %> auth status',
24
25
  '<%= config.bin %> auth status -o json',
26
+ 'PACHCA_TOKEN=$TOKEN <%= config.bin %> auth status -o json',
25
27
  ];
26
28
  static flags = {
27
29
  ...BaseCommand.baseFlags,
@@ -33,12 +35,30 @@ export default class AuthStatus extends BaseCommand {
33
35
  async run() {
34
36
  const { flags } = await this.parse(AuthStatus);
35
37
  this.parsedFlags = flags;
36
- const profileName = flags.profile || process.env.PACHCA_PROFILE || getActiveProfile();
37
38
  const format = this.getOutputFormat();
38
- if (!profileName) {
39
- outputError({ error: 'No active profile. Run: pachca auth login', type: 'PACHCA_USAGE_ERROR', code: null }, format);
39
+ // The token every other command would use, picked in the same order:
40
+ // --token, --profile, PACHCA_TOKEN, PACHCA_PROFILE, the active profile.
41
+ // Agents and CI usually pass PACHCA_TOKEN with no profile at all, and this
42
+ // is the command they are sent to check the connection.
43
+ const source = tokenSource({ token: flags.token, profile: flags.profile });
44
+ if (source.kind === 'flag') {
45
+ await this.showPassedToken(flags.token, '--token');
46
+ return;
47
+ }
48
+ if (source.kind === 'env') {
49
+ await this.showPassedToken(process.env.PACHCA_TOKEN, 'PACHCA_TOKEN');
50
+ return;
51
+ }
52
+ if (source.kind === 'none') {
53
+ outputError({
54
+ error: 'No active profile. Run: pachca auth login',
55
+ type: 'PACHCA_USAGE_ERROR',
56
+ code: null,
57
+ hint: 'pachca auth login, or set PACHCA_TOKEN',
58
+ }, format);
40
59
  this.exit(2);
41
60
  }
61
+ const profileName = source.kind === 'profile' ? source.profileName : undefined;
42
62
  const profile = getProfile(profileName);
43
63
  if (!profile) {
44
64
  outputError({ error: `Profile "${profileName}" not found`, type: 'PACHCA_USAGE_ERROR', code: null }, format);
@@ -67,6 +87,7 @@ export default class AuthStatus extends BaseCommand {
67
87
  if (format === 'json') {
68
88
  this.output({
69
89
  profile: profileName,
90
+ source: source.kind === 'profile' ? source.via : null,
70
91
  type: profile.type,
71
92
  user: profile.user,
72
93
  email: profile.email || null,
@@ -111,10 +132,88 @@ export default class AuthStatus extends BaseCommand {
111
132
  process.stderr.write(` Список прав в профиле расходился с сервером и был обновлён\n`);
112
133
  }
113
134
  }
135
+ /**
136
+ * A token passed to the command rather than saved in a profile. Nothing about it
137
+ * is stored, so everything comes from the server: whose it is and what it carries.
138
+ * A revoked token or no network ends the command with the usual error and hint.
139
+ */
140
+ async showPassedToken(token, label) {
141
+ // A PACHCA_TOKEN left in the environment silently wins over the profile the
142
+ // person thinks they are using. Name that profile, so the mix-up is visible,
143
+ // also when the forgotten token no longer works.
144
+ const profileName = label === 'PACHCA_TOKEN' ? process.env.PACHCA_PROFILE || getActiveProfile() : undefined;
145
+ const unusedProfile = profileName && listProfiles()[profileName] ? profileName : null;
146
+ const clientFlags = { ...this.getClientFlags(), quiet: true };
147
+ let info;
148
+ try {
149
+ const response = await request({ method: 'GET', path: '/oauth/token/info', token }, clientFlags);
150
+ info = response.data.data;
151
+ }
152
+ catch (error) {
153
+ // A dead token (401) or one whose owner lost access (403 access_denied):
154
+ // the profile it hides may still work, so say so.
155
+ const dead = error instanceof ApiError &&
156
+ (error.details.code === 401 || (error.details.code === 403 && error.details.validation_code === 'access_denied'));
157
+ if (dead && unusedProfile) {
158
+ const human = this.getOutputFormat() !== 'json' && process.stderr.isTTY;
159
+ error.details.unused_profile = unusedProfile;
160
+ error.details.hint = human
161
+ ? `Профиль ${unusedProfile} не используется, пока задан PACHCA_TOKEN: уберите переменную или добавьте --profile ${unusedProfile}`
162
+ : `unset PACHCA_TOKEN, or add --profile ${unusedProfile}`;
163
+ }
164
+ throw error;
165
+ }
166
+ const identity = await fetchIdentity(token, info.user_id, clientFlags);
167
+ const type = identity.isBot === null ? null : identity.isBot ? 'bot' : 'user';
168
+ const user = identity.isBot ? `${identity.name} (бот)` : identity.name;
169
+ const scopes = info.scopes ?? [];
170
+ // A CLI login or an app's token lives an hour from issue, and nothing renews a
171
+ // token passed to the command. Other tokens have no lifetime at all.
172
+ const expiresIn = info.expires_in ?? null;
173
+ const issuedAt = info.created_at ? Date.parse(info.created_at) : Number.NaN;
174
+ const expiresAt = expiresIn && !Number.isNaN(issuedAt) ? new Date(issuedAt + expiresIn * 1000) : null;
175
+ if (this.getOutputFormat() === 'json') {
176
+ this.output({
177
+ profile: null,
178
+ source: label,
179
+ type,
180
+ user,
181
+ user_id: info.user_id,
182
+ email: identity.email,
183
+ auth: 'token',
184
+ expires_at: expiresAt ? expiresAt.toISOString() : null,
185
+ expires_in: expiresIn,
186
+ scopes,
187
+ scopes_source: 'server',
188
+ storage: null,
189
+ secret_readable: true,
190
+ unused_profile: unusedProfile,
191
+ });
192
+ return;
193
+ }
194
+ const emailStr = identity.email ? ` (${identity.email})` : '';
195
+ const from = label === '--token' ? 'флага --token' : 'PACHCA_TOKEN';
196
+ const typeStr = type ?? (identity.noScope ? 'бот или сотрудник: без права profile:read не узнать' : 'бот или сотрудник: профиль не прочитан');
197
+ process.stderr.write(` Подключён как: ${user}${emailStr} [${typeStr}]\n`);
198
+ if (expiresIn) {
199
+ const minutesLeft = expiresAt ? Math.round((expiresAt.getTime() - Date.now()) / 60_000) : null;
200
+ const left = minutesLeft === null ? `живёт ${Math.round(expiresIn / 60)} мин от выдачи` : minutesLeft > 0 ? `осталось ${minutesLeft} мин` : 'истёк';
201
+ process.stderr.write(` Вход: токеном из ${from}, ${left}, CLI его не продлевает\n`);
202
+ }
203
+ else {
204
+ process.stderr.write(` Вход: готовым токеном из ${from}\n`);
205
+ }
206
+ process.stderr.write(scopes.length
207
+ ? ` Права по данным сервера (${scopes.length}): ${scopes.join(', ')}\n`
208
+ : ` Права по данным сервера: ни одного\n`);
209
+ if (unusedProfile) {
210
+ process.stderr.write(` Профиль ${unusedProfile} не используется, пока задан PACHCA_TOKEN. Чтобы работать с ним, добавьте --profile ${unusedProfile} или уберите переменную.\n`);
211
+ }
212
+ }
114
213
  /** Ask the server what the token really carries. Null when it cannot be asked. */
115
214
  async fetchRemoteScopes(token) {
116
215
  try {
117
- const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { quiet: true });
216
+ const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { ...this.getClientFlags(), quiet: true });
118
217
  const info = response.data.data;
119
218
  return info?.scopes ?? null;
120
219
  }
@@ -19,7 +19,7 @@ export default class BotsCreate extends BaseCommand {
19
19
  description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. Без поля ник собирается из имени в латинской транслитерации с `_bot` в конце, а если такой ник занят, с `_2_bot`, `_3_bot` и далее. (макс. 255 символов)",
20
20
  }),
21
21
  'outgoing-url': Flags.string({
22
- description: "URL исходящего вебхука (макс. 255 символов)",
22
+ description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. (макс. 255 символов)",
23
23
  }),
24
24
  'events': Flags.string({
25
25
  description: "События, на которые подписан бот. Без поля бот подписан на `message_new`.",
@@ -34,7 +34,7 @@ export default class BotsCreate extends BaseCommand {
34
34
  description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
35
35
  }),
36
36
  'template': Flags.string({
37
- description: "Шаблон форматирования входящего вебхука",
37
+ description: "Шаблон форматирования входящего вебхука. Шаблон с синтаксической ошибкой у источника `simple`, в том числе у пустого бота с `kind: simple`, даёт ответ `400` на этом поле с кодом `invalid`, и бот не создаётся. Причина приходит в `message`.",
38
38
  }),
39
39
  'template-engine': Flags.string({
40
40
  description: "Шаблонизатор для обработки шаблона входящего вебхука. Без этого поля бот получает `liquid`, а пустой бот (`empty: true`) — `mustache`.",