@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 +15 -0
- package/README.md +20 -0
- package/README.template.md +20 -0
- package/dist/base-command.js +27 -5
- package/dist/client.js +12 -6
- package/dist/commands/auth/login.js +11 -19
- package/dist/commands/auth/status.js +106 -7
- package/dist/commands/bots/create.js +2 -2
- package/dist/commands/bots/listen-events.js +248 -0
- package/dist/commands/bots/update-webhook.js +2 -2
- package/dist/commands/bots/update.js +2 -2
- package/dist/commands/chats/list.js +1 -1
- package/dist/commands/doctor.js +6 -6
- package/dist/commands/messages/list.js +1 -1
- package/dist/commands/users/update.js +1 -1
- package/dist/data/changelog.json +55 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/identity.js +25 -0
- package/dist/webhook-listener.js +262 -0
- package/oclif.manifest.json +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
import { ApiError, resolveTimeoutSeconds } from '../../client.js';
|
|
5
|
+
import { listenWebhookEvents } from '../../webhook-listener.js';
|
|
6
|
+
/** A `view_submit` payload carries what the answer to the form needs. */
|
|
7
|
+
function formSubmit(payload) {
|
|
8
|
+
const p = payload;
|
|
9
|
+
if (!p || p.type !== 'view' || p.event !== 'submit')
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof p.view_id !== 'string' || typeof p.submit_id !== 'string')
|
|
12
|
+
return undefined;
|
|
13
|
+
return { view_id: p.view_id, submit_id: p.submit_id };
|
|
14
|
+
}
|
|
15
|
+
/** Pachca stamps every delivery attempt anew, so a record replayed from the history gets the time of forwarding. */
|
|
16
|
+
function withFreshTimestamp(payload) {
|
|
17
|
+
const p = payload;
|
|
18
|
+
if (!p || typeof p !== 'object' || Array.isArray(p) || typeof p.webhook_timestamp !== 'number')
|
|
19
|
+
return payload;
|
|
20
|
+
return { ...p, webhook_timestamp: Math.floor(Date.now() / 1000) };
|
|
21
|
+
}
|
|
22
|
+
// Pachca takes the answer to a form for 5 seconds and then shows the employee a sending error.
|
|
23
|
+
// A submission older than this is one the employee saw fail. The age is measured on Pachca's clock,
|
|
24
|
+
// from the Date header of the last API response: 10 s leave room for its whole seconds and transit.
|
|
25
|
+
const FORM_STALE_MS = 10_000;
|
|
26
|
+
// With no Date header the local clock may differ from Pachca's, so the margin is wider.
|
|
27
|
+
const FORM_STALE_LOCAL_MS = 60_000;
|
|
28
|
+
export default class BotsListenEvents extends BaseCommand {
|
|
29
|
+
static summary = 'Слушать события бота в реальном времени';
|
|
30
|
+
static description = `Слушать события бота: JSONL в stdout или пересылка на локальный HTTP-обработчик.
|
|
31
|
+
|
|
32
|
+
Нужен токен бота с webhooks:events:read и включённая история событий.
|
|
33
|
+
Без --forward-to записи остаются в истории. С --forward-to запись удаляется из истории после ответа 2xx.
|
|
34
|
+
Для этого нужно право webhooks:events:delete. Если задан PACHCA_SIGNING_SECRET, запрос подписан им, как вебхук.
|
|
35
|
+
Ответ обработчика на отправку формы CLI передаёт в Пачку, для этого нужно право views:write.
|
|
36
|
+
Ошибка обработчика останавливает команду. После перезапуска событие может прийти снова.`;
|
|
37
|
+
static scope = 'webhooks:events:read';
|
|
38
|
+
static examples = [
|
|
39
|
+
'PACHCA_TOKEN=$PACHCA_BOT_TOKEN <%= config.bin %> bots listen-events',
|
|
40
|
+
'PACHCA_TOKEN=$PACHCA_BOT_TOKEN PACHCA_SIGNING_SECRET=$PACHCA_SIGNING_SECRET <%= config.bin %> bots listen-events --forward-to http://localhost:3000/webhook',
|
|
41
|
+
];
|
|
42
|
+
static flags = {
|
|
43
|
+
...BaseCommand.baseFlags,
|
|
44
|
+
'forward-to': Flags.string({ description: 'HTTP(S)-адрес обработчика: событие уходит POST, запись удаляется из истории после 2xx' }),
|
|
45
|
+
'no-retry': Flags.boolean({
|
|
46
|
+
description: 'Не переподключаться к потоку после обрыва. Запросы к API по-прежнему повторяются при 429 и 5xx',
|
|
47
|
+
default: false,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
/** Without views:write the answer to a form cannot reach Pachca, and trying would fail the record. */
|
|
51
|
+
answerForms = true;
|
|
52
|
+
/** Pachca's clock minus the local one, from the Date header of the last API response, with the local stamps it was taken at. */
|
|
53
|
+
serverClock;
|
|
54
|
+
// The stream response contains a capability URL: suppress HTTP body diagnostics for this command.
|
|
55
|
+
// --no-retry here means «do not reconnect»: single API calls keep their retries on 429 and 5xx.
|
|
56
|
+
getClientFlags() {
|
|
57
|
+
return { ...super.getClientFlags(), verbose: false, quiet: true, 'no-retry': false };
|
|
58
|
+
}
|
|
59
|
+
async run() {
|
|
60
|
+
const { flags } = await this.parse(BotsListenEvents);
|
|
61
|
+
this.parsedFlags = flags;
|
|
62
|
+
let target;
|
|
63
|
+
if (flags['forward-to']) {
|
|
64
|
+
try {
|
|
65
|
+
target = new URL(flags['forward-to']);
|
|
66
|
+
}
|
|
67
|
+
catch { /* validation below */ }
|
|
68
|
+
if (!target || !['http:', 'https:'].includes(target.protocol) || target.username || target.password) {
|
|
69
|
+
this.validationError([{ message: 'Нужен HTTP(S)-адрес без логина и пароля', flag: 'forward-to' }]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if ((flags.output && flags.output !== 'json') || flags.plain || flags.columns || flags.quiet) {
|
|
73
|
+
this.validationError([{ message: 'События выводятся в JSONL; форматирование и --quiet не поддерживаются' }]);
|
|
74
|
+
}
|
|
75
|
+
if (flags['dry-run']) {
|
|
76
|
+
await this.apiRequest({ method: 'GET', path: '/webhooks/events/stream' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (target)
|
|
80
|
+
await this.checkForwardingScopes();
|
|
81
|
+
// The same rule as API calls: --timeout, defaults.timeout, PACHCA_TIMEOUT, 30 seconds.
|
|
82
|
+
const timeoutMs = Math.ceil(resolveTimeoutSeconds(this.getClientFlags().timeout) * 1_000);
|
|
83
|
+
const secret = process.env.PACHCA_SIGNING_SECRET || undefined;
|
|
84
|
+
if (target && !secret) {
|
|
85
|
+
process.stderr.write('PACHCA_SIGNING_SECRET не задан: события уходят без заголовка Pachca-Signature\n');
|
|
86
|
+
}
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
let interrupted = false;
|
|
89
|
+
const stop = () => { interrupted = true; controller.abort(); };
|
|
90
|
+
process.once('SIGINT', stop);
|
|
91
|
+
process.once('SIGTERM', stop);
|
|
92
|
+
try {
|
|
93
|
+
await listenWebhookEvents({
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
retry: !flags['no-retry'],
|
|
96
|
+
timeoutMs,
|
|
97
|
+
acknowledge: !!target,
|
|
98
|
+
request: async (opts) => {
|
|
99
|
+
const response = await this.apiRequest(opts);
|
|
100
|
+
const date = Date.parse(response.headers?.get('date') ?? '');
|
|
101
|
+
if (Number.isFinite(date)) {
|
|
102
|
+
const wall = Date.now();
|
|
103
|
+
this.serverClock = { offset: date - wall, wall, mono: performance.now() };
|
|
104
|
+
}
|
|
105
|
+
return response.data;
|
|
106
|
+
},
|
|
107
|
+
onReconnect: () => { process.stderr.write('Переподключение к потоку событий…\n'); },
|
|
108
|
+
onEvent: async (event) => {
|
|
109
|
+
if (!target) {
|
|
110
|
+
await new Promise((resolve, reject) => {
|
|
111
|
+
process.stdout.write(JSON.stringify(event) + '\n', (error) => error ? reject(error) : resolve());
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await this.forward(event, target, secret, controller.signal, timeoutMs);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
controller.abort();
|
|
121
|
+
process.removeListener('SIGINT', stop);
|
|
122
|
+
process.removeListener('SIGTERM', stop);
|
|
123
|
+
}
|
|
124
|
+
if (interrupted) {
|
|
125
|
+
// An API call in flight would keep the process alive until its timeout and retries end.
|
|
126
|
+
// A JSONL line may still wait in the pipe: let it out, but do not wait on a stuck reader.
|
|
127
|
+
await Promise.race([
|
|
128
|
+
new Promise((resolve) => { process.stdout.write('', () => resolve()); }),
|
|
129
|
+
new Promise((resolve) => { setTimeout(resolve, 1_000); }),
|
|
130
|
+
]);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Without webhooks:events:delete every forwarded event would come again on the next run. */
|
|
135
|
+
async checkForwardingScopes() {
|
|
136
|
+
const body = (await this.apiRequest({ method: 'GET', path: '/oauth/token/info' })).data;
|
|
137
|
+
const scopes = body?.data?.scopes ?? [];
|
|
138
|
+
const has = (scope) => scopes.includes(scope) || scopes.includes('all');
|
|
139
|
+
if (!has('webhooks:events:delete')) {
|
|
140
|
+
throw new ApiError({
|
|
141
|
+
error: 'Для --forward-to токену нужно право webhooks:events:delete: без него события придут снова при каждом запуске',
|
|
142
|
+
code: null,
|
|
143
|
+
type: 'PACHCA_SCOPE_ERROR',
|
|
144
|
+
scope: 'webhooks:events:delete',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
this.answerForms = has('views:write');
|
|
148
|
+
if (!this.answerForms) {
|
|
149
|
+
process.stderr.write('У токена нет права views:write: ответ обработчика на отправку формы не дойдёт до Пачки, и сотрудник увидит ошибку отправки\n');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async forward(event, target, secret, signal, timeoutMs) {
|
|
153
|
+
const name = `${event.id} (${event.event_type})`;
|
|
154
|
+
const form = formSubmit(event.payload);
|
|
155
|
+
const clock = this.serverClock;
|
|
156
|
+
// A local clock that jumped (time sync, a virtual machine catching up, a manual change) or a machine
|
|
157
|
+
// that slept since the stamp breaks the offset: the wider margin on the local clock applies then.
|
|
158
|
+
const offset = clock && Math.abs((Date.now() - clock.wall) - (performance.now() - clock.mono)) <= 1_000
|
|
159
|
+
? clock.offset : undefined;
|
|
160
|
+
const age = Date.now() + (offset ?? 0) - Date.parse(event.created_at);
|
|
161
|
+
if (form && age > (offset === undefined ? FORM_STALE_LOCAL_MS : FORM_STALE_MS)) {
|
|
162
|
+
// The record is acknowledged: no answer reaches Pachca now, and the employee may have sent the form again.
|
|
163
|
+
process.stderr.write(`Отправка формы ${name} пропущена: срок ответа вышел, сотрудник уже видел ошибку отправки\n`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const payload = withFreshTimestamp(event.payload);
|
|
167
|
+
const body = JSON.stringify(payload);
|
|
168
|
+
// The record id stays the same for every attempt and every run, like the key of one webhook delivery.
|
|
169
|
+
const headers = { 'content-type': 'application/json', 'idempotency-key': event.id };
|
|
170
|
+
if (secret)
|
|
171
|
+
headers['pachca-signature'] = createHmac('sha256', secret).update(body).digest('hex');
|
|
172
|
+
let response;
|
|
173
|
+
try {
|
|
174
|
+
response = await fetch(target, {
|
|
175
|
+
method: 'POST', headers, body, redirect: 'manual',
|
|
176
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
if (signal.aborted)
|
|
181
|
+
return;
|
|
182
|
+
throw new Error(`Не удалось переслать событие ${name}, запись сохранена в истории`);
|
|
183
|
+
}
|
|
184
|
+
if (form && (response.status === 400 || response.status === 422)) {
|
|
185
|
+
const errors = await this.formErrors(response);
|
|
186
|
+
if (!errors)
|
|
187
|
+
throw new Error(`Обработчик ответил HTTP ${response.status} на форму ${name} без объекта errors, запись сохранена в истории`);
|
|
188
|
+
await this.answerForm(form, errors);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await response.body?.cancel();
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw new Error(`Обработчик ответил HTTP ${response.status} на событие ${name}, запись сохранена в истории`);
|
|
194
|
+
if (form)
|
|
195
|
+
await this.answerForm(form);
|
|
196
|
+
}
|
|
197
|
+
/** `errors` from a 400 or 422 reply: field name to the text shown under that field. */
|
|
198
|
+
async formErrors(response) {
|
|
199
|
+
try {
|
|
200
|
+
const body = await response.json();
|
|
201
|
+
const errors = body?.errors;
|
|
202
|
+
if (!errors || typeof errors !== 'object' || Array.isArray(errors))
|
|
203
|
+
return undefined;
|
|
204
|
+
const entries = Object.entries(errors);
|
|
205
|
+
if (!entries.length || !entries.every(([, value]) => typeof value === 'string'))
|
|
206
|
+
return undefined;
|
|
207
|
+
return Object.fromEntries(entries);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A bot without a webhook URL closes the form with a method: the reply a handler gives to a
|
|
215
|
+
* webhook goes on to Pachca the same way, closing the form or showing errors under its fields.
|
|
216
|
+
* The handler has already acted on the submission, and the 5 seconds for an answer do not come
|
|
217
|
+
* back, so a refused answer is reported and the record is still acknowledged.
|
|
218
|
+
*/
|
|
219
|
+
async answerForm(form, errors) {
|
|
220
|
+
if (!this.answerForms)
|
|
221
|
+
return;
|
|
222
|
+
// 503: Pachca kept the submission open, so the same call a moment later can still close the form.
|
|
223
|
+
// Repeating is safe: an answer already taken, or one past the 5 seconds, gets 410.
|
|
224
|
+
for (let attempt = 0;; attempt++) {
|
|
225
|
+
try {
|
|
226
|
+
await this.apiRequest({
|
|
227
|
+
method: 'POST',
|
|
228
|
+
path: `/views/${encodeURIComponent(form.view_id)}/submit_response`,
|
|
229
|
+
body: { submit_id: form.submit_id, ...(errors ? { errors } : {}) },
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (!(error instanceof ApiError))
|
|
235
|
+
throw error;
|
|
236
|
+
if (error.details.code === 503 && attempt < 2) {
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const reason = error.details.code === 410
|
|
241
|
+
? 'время вышло или ответ уже отправлен'
|
|
242
|
+
: `HTTP ${error.details.code ?? 'без ответа'}: ${error.details.error}`;
|
|
243
|
+
process.stderr.write(`Ответ на форму ${form.submit_id} не передан в Пачку: ${reason}\n`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -13,14 +13,14 @@ export default class BotsUpdateWebhook extends BaseCommand {
|
|
|
13
13
|
static flags = {
|
|
14
14
|
...BaseCommand.baseFlags,
|
|
15
15
|
'outgoing-url': Flags.string({
|
|
16
|
-
description: "URL исходящего
|
|
16
|
+
description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. Пустая строка отключает вебхук. (макс. 255 символов)",
|
|
17
17
|
}),
|
|
18
18
|
};
|
|
19
19
|
async run() {
|
|
20
20
|
const { args, flags } = await this.parse(BotsUpdateWebhook);
|
|
21
21
|
this.parsedFlags = flags;
|
|
22
22
|
const missingRequired = [
|
|
23
|
-
{ flag: 'outgoing-url', label: "URL исходящего
|
|
23
|
+
{ flag: 'outgoing-url', label: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. Пустая строка отключает вебхук.", type: 'string' },
|
|
24
24
|
].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
|
|
25
25
|
if (missingRequired.length > 0) {
|
|
26
26
|
if (this.isInteractive()) {
|
|
@@ -23,7 +23,7 @@ export default class BotsUpdate extends BaseCommand {
|
|
|
23
23
|
description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. (макс. 255 символов)",
|
|
24
24
|
}),
|
|
25
25
|
'outgoing-url': Flags.string({
|
|
26
|
-
description: "URL исходящего
|
|
26
|
+
description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. (макс. 255 символов)",
|
|
27
27
|
}),
|
|
28
28
|
'events': Flags.string({
|
|
29
29
|
description: "События, на которые подписан бот. Список заменяется целиком.",
|
|
@@ -38,7 +38,7 @@ export default class BotsUpdate extends BaseCommand {
|
|
|
38
38
|
description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
|
|
39
39
|
}),
|
|
40
40
|
'template': Flags.string({
|
|
41
|
-
description: "Шаблон форматирования входящего
|
|
41
|
+
description: "Шаблон форматирования входящего вебхука. Шаблон проверяется, когда в запросе меняются `template`, `template_engine` или `kind`, поэтому смена одного шаблонизатора или включение `kind: simple` проверяет и уже сохранённый шаблон. Шаблон с синтаксической ошибкой у источника `simple` даёт ответ `400` на этом поле с кодом `invalid`, и запрос не применяется. Причина приходит в `message`.",
|
|
42
42
|
}),
|
|
43
43
|
'template-engine': Flags.string({
|
|
44
44
|
description: "Шаблонизатор для обработки шаблона входящего вебхука",
|
|
@@ -16,7 +16,7 @@ export default class ChatsList extends BaseCommand {
|
|
|
16
16
|
default: "id",
|
|
17
17
|
}),
|
|
18
18
|
'order': Flags.string({
|
|
19
|
-
description: "Направление
|
|
19
|
+
description: "Направление сортировки по полю `sort`, например `sort=last_message_at&order=desc`. Без `sort` направление применяется к `id`.",
|
|
20
20
|
options: ["asc", "desc"],
|
|
21
21
|
default: "desc",
|
|
22
22
|
}),
|
package/dist/commands/doctor.js
CHANGED
|
@@ -22,14 +22,14 @@ export default class Doctor extends BaseCommand {
|
|
|
22
22
|
let networkOk = false;
|
|
23
23
|
// 1. Node.js version
|
|
24
24
|
const nodeVersion = process.version.replace('v', '');
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
checks.push({ name: 'node', status: 'ok', version: nodeVersion, required: '>=20' });
|
|
28
|
-
this.printCheck('ok', `Node.js v${nodeVersion} (требуется >=20)`, format);
|
|
25
|
+
// AbortSignal.any (bots listen-events --forward-to) arrived in Node 20.3.0.
|
|
26
|
+
if (semver.satisfies(nodeVersion, '>=20.3.0')) {
|
|
27
|
+
checks.push({ name: 'node', status: 'ok', version: nodeVersion, required: '>=20.3.0' });
|
|
28
|
+
this.printCheck('ok', `Node.js v${nodeVersion} (требуется >=20.3.0)`, format);
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
|
-
checks.push({ name: 'node', status: 'error', version: nodeVersion, required: '>=20' });
|
|
32
|
-
this.printCheck('error', `Node.js v${nodeVersion} — требуется >=20`, format);
|
|
31
|
+
checks.push({ name: 'node', status: 'error', version: nodeVersion, required: '>=20.3.0' });
|
|
32
|
+
this.printCheck('error', `Node.js v${nodeVersion} — требуется >=20.3.0`, format);
|
|
33
33
|
}
|
|
34
34
|
// 2. Network. Check the host the CLI actually talks to: on-premise installs
|
|
35
35
|
// point elsewhere, and probing the cloud there reports a healthy network
|
|
@@ -26,7 +26,7 @@ export default class MessagesList extends BaseCommand {
|
|
|
26
26
|
default: "id",
|
|
27
27
|
}),
|
|
28
28
|
'order': Flags.string({
|
|
29
|
-
description: "Направление
|
|
29
|
+
description: "Направление сортировки по `id`: `asc` отдаёт сообщения от старых к новым, `desc` от новых к старым.",
|
|
30
30
|
options: ["asc", "desc"],
|
|
31
31
|
default: "desc",
|
|
32
32
|
}),
|
|
@@ -40,7 +40,7 @@ export default class UsersUpdate extends BaseCommand {
|
|
|
40
40
|
description: "Уровень доступа",
|
|
41
41
|
}),
|
|
42
42
|
'chat-ids': Flags.string({
|
|
43
|
-
description: "Чат, в котором останется сотрудник при переводе в гости (`role: \"guest\"`): ровно одна активная беседа или канал, куда у токена есть право добавлять участников. Без такого чата ответ `400` по этому полю, и не меняется ничего. При других ролях и у того, кто уже гость, поле не учитывается.",
|
|
43
|
+
description: "Чат, в котором останется сотрудник при переводе в гости (`role: \"guest\"`): ровно одна активная беседа или канал, куда у токена есть право добавлять участников. Без такого чата ответ `400` по этому полю, и не меняется ничего. При других ролях и у того, кто уже гость, поле не учитывается. При переводе в гости сотруднику с указанной почтой уходит письмо о смене роли со ссылкой на этот чат, даже если он ещё не входил в Пачку или приостановлен. В суточный лимит писем-приглашений оно не идёт.",
|
|
44
44
|
}),
|
|
45
45
|
'suspended': Flags.boolean({
|
|
46
46
|
description: "Деактивация пользователя. Владельца пространства приостановить нельзя никому, в том числе ему самому: ответ `403`.",
|
package/dist/data/changelog.json
CHANGED
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "2026.11.2",
|
|
4
|
+
"date": "10 ноября 2026",
|
|
5
|
+
"changes": [
|
|
6
|
+
{
|
|
7
|
+
"type": "+",
|
|
8
|
+
"command": "bots listen-events",
|
|
9
|
+
"description": "События бота в реальном времени без публичного адреса: JSONL в терминале или пересылка на локальный обработчик, как исходящий вебхук, с подписью, свежим `webhook_timestamp` и ответом на отправку формы. Запись удаляется из истории после ответа 2xx."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "~",
|
|
13
|
+
"description": "Для запуска CLI требуется Node.js 20.3.0 или новее"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "2026.11.1",
|
|
19
|
+
"date": "9 ноября 2026",
|
|
20
|
+
"changes": [
|
|
21
|
+
{
|
|
22
|
+
"type": "~",
|
|
23
|
+
"command": "auth status",
|
|
24
|
+
"description": "Показывает токен, с которым работают команды, в том числе из `--token` и `PACHCA_TOKEN`. Владельца, права и срок жизни такого токена команда спрашивает у сервера и называет сохранённый профиль, который не используется, пока задан `PACHCA_TOKEN`."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "~",
|
|
28
|
+
"description": "Подсказки к отказу `insufficient_scope` ведут к `pachca auth status` с тем же `--token` или `--profile`, что у команды, а для сохранённого профиля в `-o json` ещё и с `--remote`"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "~",
|
|
32
|
+
"command": "auth login",
|
|
33
|
+
"description": "Подсказка `pachca auth status` после входа называет профиль, если без него команда покажет другой токен, а при заданном `PACHCA_TOKEN` вход предупреждает, что команды используют переменную"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "+",
|
|
37
|
+
"command": "messages get, messages list, messages create, messages update, search list-messages",
|
|
38
|
+
"description": "В ответах с сообщениями есть поля `source` и `application`: через что сотрудник отправил сообщение и какая программа"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "~",
|
|
42
|
+
"command": "chats list, messages list",
|
|
43
|
+
"description": "`--order` без `--sort` задаёт направление по `id`"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"type": "~",
|
|
47
|
+
"command": "bots create, bots update, bots update-webhook",
|
|
48
|
+
"description": "В описании адреса вебхука коды отказа, а в описании шаблона проверка синтаксиса"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "~",
|
|
52
|
+
"command": "users update",
|
|
53
|
+
"description": "В описании чата для перевода в гости письмо о смене роли"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
2
57
|
{
|
|
3
58
|
"version": "2026.11.0",
|
|
4
59
|
"date": "4 ноября 2026",
|