@axon-assistant/core 2026.8.25 → 2026.8.27

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
@@ -32,6 +32,7 @@ axon start --host 0.0.0.0 # ещё и для устройств из се
32
32
  использования.
33
33
 
34
34
  ```bash
35
+ axon code # новый код подключения устройства
35
36
  axon status # запущено ли и где
36
37
  axon secret set provider.anthropic.apiKey sk-… # ключ провайдера
37
38
  axon devices # подключённые устройства
package/dist/Daemon.d.ts CHANGED
@@ -57,6 +57,11 @@ export declare class Daemon {
57
57
  * ответ, и мог бы показать его только целиком в самом конце.
58
58
  */
59
59
  private readonly signalListeners;
60
+ /**
61
+ * Пропуск для того, кто сидит за этой машиной: им подписан запрос на новый
62
+ * код подключения. Живёт в файле рядом с базой, с правами 0600.
63
+ */
64
+ private readonly controlToken;
60
65
  /** Разрешается, когда все установленные плагины отработали запуск. */
61
66
  pluginsReady: Promise<void>;
62
67
  constructor(options?: DaemonOptions);
@@ -116,6 +121,14 @@ export declare class Daemon {
116
121
  * же машине мог его найти, не завися от того, кто его поднял, адрес и pid
117
122
  * кладутся в файл рядом с базой.
118
123
  */
124
+ private controlPath;
125
+ /**
126
+ * Пропуск переписывается на каждом запуске: старый файл, оставшийся от
127
+ * убитого процесса, не должен открывать двери в новое ядро.
128
+ */
129
+ private writeControlToken;
130
+ /** Сравнение постоянного времени: пропуск подбирают по одному символу. */
131
+ private controlAllowed;
119
132
  private announce;
120
133
  private withdraw;
121
134
  stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"Daemon.d.ts","sourceRoot":"","sources":["../src/Daemon.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAKtD,OAAO,EAML,OAAO,EAER,MAAM,gBAAgB,CAAC;AAKxB,eAAO,MAAM,cAAc,QAC4C,CAAC;AAIxE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAoCD;;;;;;;;;;GAUG;AACH,qBAAa,MAAM;IAyBL,OAAO,CAAC,QAAQ,CAAC,OAAO;IAxBpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,QAAQ,CAAgC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,OAAO,CAAC,IAAI,CAA4B;IACxC;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuC;IACvE,sEAAsE;IACtE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAqB;gBAEnB,OAAO,GAAE,aAAkB;IAsClD,KAAK,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAoDhF;;;;;;;OAOG;YACW,YAAY;IAmC1B;;;;;OAKG;YACW,WAAW;IAkBzB;;;;;OAKG;IACG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;IAoC3E,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrC,cAAc;;;;IAOd;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,QAAQ;IAIV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B,IAAI,GAAG,IAAI,MAAM,CAGhB;IAED,4EAA4E;IAC5E,OAAO,CAAC,WAAW;YAWL,SAAS;IAsDvB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,SAAS;CA+BlB"}
1
+ {"version":3,"file":"Daemon.d.ts","sourceRoot":"","sources":["../src/Daemon.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAKtD,OAAO,EAML,OAAO,EAER,MAAM,gBAAgB,CAAC;AAKxB,eAAO,MAAM,cAAc,QAC4C,CAAC;AAIxE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAoCD;;;;;;;;;;GAUG;AACH,qBAAa,MAAM;IA8BL,OAAO,CAAC,QAAQ,CAAC,OAAO;IA7BpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,QAAQ,CAAgC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,OAAO,CAAC,IAAI,CAA4B;IACxC;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuC;IACvE;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0C;IACvE,sEAAsE;IACtE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAqB;gBAEnB,OAAO,GAAE,aAAkB;IAsClD,KAAK,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAqDhF;;;;;;;OAOG;YACW,YAAY;IAmC1B;;;;;OAKG;YACW,WAAW;IAkBzB;;;;;OAKG;IACG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;IAoC3E,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrC,cAAc;;;;IAOd;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;IAInB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAKzB,0EAA0E;IAC1E,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,QAAQ;IAIV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B,IAAI,GAAG,IAAI,MAAM,CAGhB;IAED,4EAA4E;IAC5E,OAAO,CAAC,WAAW;YAWL,SAAS;IAuHvB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,SAAS;CA+BlB"}
package/dist/cli.js CHANGED
@@ -680,10 +680,33 @@ var SecretStore = class {
680
680
  const row = this.db.prepare(`SELECT * FROM secrets WHERE key = ?`).get(key);
681
681
  if (!row)
682
682
  return null;
683
+ return this.decrypt(row);
684
+ }
685
+ /**
686
+ * Расшифровать строку. Бросает, если значение не сходится с ключом.
687
+ *
688
+ * GCM проверяет подпись, поэтому чужой ключ шифрования — это не мусор на
689
+ * выходе, а честный отказ. Ловить его здесь нельзя: молча вернуть `null`
690
+ * значит превратить «ключ не читается» в «ключа нет», а лечатся они
691
+ * по-разному.
692
+ */
693
+ decrypt(row) {
683
694
  const decipher = crypto.createDecipheriv(ALGORITHM, this.masterKey(), row.iv);
684
695
  decipher.setAuthTag(row.tag);
685
696
  return Buffer.concat([decipher.update(row.ciphertext), decipher.final()]).toString("utf8");
686
697
  }
698
+ /** Читается ли значение здешним ключом шифрования. */
699
+ readable(key) {
700
+ const row = this.db.prepare(`SELECT * FROM secrets WHERE key = ?`).get(key);
701
+ if (!row)
702
+ return false;
703
+ try {
704
+ this.decrypt(row);
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
687
710
  delete(key) {
688
711
  this.db.prepare(`DELETE FROM secrets WHERE key = ?`).run(key);
689
712
  }
@@ -704,7 +727,12 @@ var SecretStore = class {
704
727
  key,
705
728
  set: true,
706
729
  ...row.hint ? { hint: row.hint } : {},
707
- updatedAt: row.updated_at
730
+ updatedAt: row.updated_at,
731
+ // Наличие строки в базе — ещё не рабочий секрет: база могла приехать
732
+ // с другой машины, а ключ шифрования остаться там. «Ключ на месте» в
733
+ // таком случае врало, и человек искал причину в чём угодно, кроме
734
+ // переноса.
735
+ ...this.readable(key) ? {} : { unreadable: true }
708
736
  };
709
737
  });
710
738
  }
@@ -1861,7 +1889,7 @@ import fs15 from "node:fs";
1861
1889
  // ../protocol/dist/primitives.js
1862
1890
  import { z } from "zod";
1863
1891
  var PROTOCOL_VERSION = 1;
1864
- var PROTOCOL_REVISION = 10;
1892
+ var PROTOCOL_REVISION = 11;
1865
1893
  var zTimestamp = z.string().datetime();
1866
1894
  var zId = z.string().uuid();
1867
1895
  var zExternalId = z.string().min(1).max(256);
@@ -2907,7 +2935,15 @@ var zSecretStatus = z10.object({
2907
2935
  set: z10.boolean(),
2908
2936
  /** Последние символы значения. Присутствует, только если секрет задан. */
2909
2937
  hint: z10.string().max(8).optional(),
2910
- updatedAt: zTimestamp.optional()
2938
+ updatedAt: zTimestamp.optional(),
2939
+ /**
2940
+ * Значение лежит, но расшифровать его нечем.
2941
+ *
2942
+ * Так бывает после переноса: база приехала, а ключ шифрования рядом с ней —
2943
+ * нет (`axon backup` не кладёт его в архив без спроса). Снаружи это выглядело
2944
+ * как «ключ на месте», хотя ни один запрос с ним не проходит.
2945
+ */
2946
+ unreadable: z10.boolean().optional()
2911
2947
  });
2912
2948
  var zSettingsGetReq = z10.object({
2913
2949
  /** Какие ключи вернуть. Пусто — все, к которым у устройства есть доступ. */
@@ -3424,8 +3460,8 @@ function estimateTokens(text) {
3424
3460
  return 0;
3425
3461
  let cyrillic = 0;
3426
3462
  for (const char of text) {
3427
- const code2 = char.codePointAt(0);
3428
- if (code2 >= 1024 && code2 <= 1279)
3463
+ const code3 = char.codePointAt(0);
3464
+ if (code3 >= 1024 && code3 <= 1279)
3429
3465
  cyrillic++;
3430
3466
  }
3431
3467
  const other = text.length - cyrillic;
@@ -5161,9 +5197,9 @@ var HOST_READY = "host.ready";
5161
5197
  // ../core/dist/plugins/rpc.js
5162
5198
  var RpcError = class extends Error {
5163
5199
  code;
5164
- constructor(message, code2 = "plugin_error") {
5200
+ constructor(message, code3 = "plugin_error") {
5165
5201
  super(message);
5166
- this.code = code2;
5202
+ this.code = code3;
5167
5203
  this.name = "RpcError";
5168
5204
  }
5169
5205
  };
@@ -5545,9 +5581,9 @@ var CLIENT_INFO = { name: "axon", version: "1" };
5545
5581
  var DEFAULT_TIMEOUT_MS = 6e4;
5546
5582
  var McpError = class extends Error {
5547
5583
  code;
5548
- constructor(message, code2 = "mcp_error") {
5584
+ constructor(message, code3 = "mcp_error") {
5549
5585
  super(message);
5550
- this.code = code2;
5586
+ this.code = code3;
5551
5587
  this.name = "McpError";
5552
5588
  }
5553
5589
  };
@@ -5659,10 +5695,10 @@ var McpClient = class {
5659
5695
  }
5660
5696
  });
5661
5697
  child.on("error", (error) => this.fail(new McpError(error.message, "spawn_failed")));
5662
- child.on("exit", (code2, signal) => {
5698
+ child.on("exit", (code3, signal) => {
5663
5699
  if (this.closed)
5664
5700
  return;
5665
- this.fail(new McpError(`\u0421\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F (${signal ?? code2})`, "exited"));
5701
+ this.fail(new McpError(`\u0421\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F (${signal ?? code3})`, "exited"));
5666
5702
  });
5667
5703
  }
5668
5704
  /** Разобрать поток stdout на строки: одна строка — одно сообщение JSON-RPC. */
@@ -6009,19 +6045,19 @@ var PluginProcess = class {
6009
6045
  clearTimeout(timer);
6010
6046
  reject(error);
6011
6047
  });
6012
- child.once("exit", (code2, signal) => {
6048
+ child.once("exit", (code3, signal) => {
6013
6049
  clearTimeout(timer);
6014
- reject(new Error(`\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0434\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 (${signal ?? code2})`));
6050
+ reject(new Error(`\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0434\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 (${signal ?? code3})`));
6015
6051
  });
6016
6052
  });
6017
- child.once("exit", (code2, signal) => {
6053
+ child.once("exit", (code3, signal) => {
6018
6054
  this.child = null;
6019
6055
  this.ready = false;
6020
6056
  this.peer.dispose(new RpcError("\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D", "not_running"));
6021
6057
  if (this.stopping)
6022
6058
  return;
6023
- const reason = signal ? `\u0443\u0431\u0438\u0442 \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C ${signal}` : `\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441 \u043A\u043E\u0434\u043E\u043C ${code2}`;
6024
- this.log.warn({ code: code2, signal }, "\u043F\u043B\u0430\u0433\u0438\u043D \u0443\u043F\u0430\u043B");
6059
+ const reason = signal ? `\u0443\u0431\u0438\u0442 \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C ${signal}` : `\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441 \u043A\u043E\u0434\u043E\u043C ${code3}`;
6060
+ this.log.warn({ code: code3, signal }, "\u043F\u043B\u0430\u0433\u0438\u043D \u0443\u043F\u0430\u043B");
6025
6061
  this.options.onExit(reason);
6026
6062
  });
6027
6063
  await started;
@@ -6723,9 +6759,9 @@ var LoadedPlugin = class {
6723
6759
  // ../core/dist/plugins/PluginHost.js
6724
6760
  var PluginError = class extends Error {
6725
6761
  code;
6726
- constructor(message, code2 = "plugin_error") {
6762
+ constructor(message, code3 = "plugin_error") {
6727
6763
  super(message);
6728
- this.code = code2;
6764
+ this.code = code3;
6729
6765
  this.name = "PluginError";
6730
6766
  }
6731
6767
  };
@@ -7677,9 +7713,9 @@ function dayAt(from, offsetDays, time) {
7677
7713
  var TICK_MS2 = 3e4;
7678
7714
  var RoutineError = class extends Error {
7679
7715
  code;
7680
- constructor(message, code2 = "routine_error") {
7716
+ constructor(message, code3 = "routine_error") {
7681
7717
  super(message);
7682
- this.code = code2;
7718
+ this.code = code3;
7683
7719
  this.name = "RoutineError";
7684
7720
  }
7685
7721
  };
@@ -9571,7 +9607,7 @@ function runCommand(input) {
9571
9607
  input.signal.removeEventListener("abort", onAbort);
9572
9608
  reject(e);
9573
9609
  });
9574
- child.on("close", (code2) => {
9610
+ child.on("close", (code3) => {
9575
9611
  if (settled)
9576
9612
  return;
9577
9613
  settled = true;
@@ -9583,7 +9619,7 @@ function runCommand(input) {
9583
9619
  resolve({
9584
9620
  stdout: decodeOutput(Buffer.concat(stdout)).trimEnd(),
9585
9621
  stderr: decodeOutput(Buffer.concat(stderr)).trimEnd(),
9586
- code: code2
9622
+ code: code3
9587
9623
  });
9588
9624
  });
9589
9625
  });
@@ -10100,6 +10136,7 @@ axon plugin logs ${id}
10100
10136
  }
10101
10137
 
10102
10138
  // src/Daemon.ts
10139
+ import crypto4 from "node:crypto";
10103
10140
  import http from "node:http";
10104
10141
  import fs18 from "node:fs";
10105
10142
  import os7 from "node:os";
@@ -10128,11 +10165,11 @@ var PairingService = class {
10128
10165
  fs17.rmSync(this.bootstrapPath, { force: true });
10129
10166
  return null;
10130
10167
  }
10131
- const code2 = generateCode();
10168
+ const code3 = generateCode();
10132
10169
  fs17.mkdirSync(path17.dirname(this.bootstrapPath), { recursive: true });
10133
- fs17.writeFileSync(this.bootstrapPath, `${code2}
10170
+ fs17.writeFileSync(this.bootstrapPath, `${code3}
10134
10171
  `, { mode: 384 });
10135
- this.pending.set(code2, {
10172
+ this.pending.set(code3, {
10136
10173
  name: "\u041F\u0435\u0440\u0432\u043E\u0435 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E",
10137
10174
  platform: "desktop",
10138
10175
  // Первому устройству — всё: с него настраивают остальные.
@@ -10147,24 +10184,24 @@ var PairingService = class {
10147
10184
  ],
10148
10185
  expiresAt: Number.POSITIVE_INFINITY
10149
10186
  });
10150
- return code2;
10187
+ return code3;
10151
10188
  }
10152
10189
  begin(input) {
10153
- const code2 = generateCode();
10154
- this.pending.set(code2, {
10190
+ const code3 = generateCode();
10191
+ this.pending.set(code3, {
10155
10192
  name: input.name,
10156
10193
  platform: input.platform,
10157
10194
  scopes: input.scopes,
10158
10195
  expiresAt: Date.now() + input.ttlSeconds * 1e3
10159
10196
  });
10160
- return { code: code2, expiresInSeconds: input.ttlSeconds };
10197
+ return { code: code3, expiresInSeconds: input.ttlSeconds };
10161
10198
  }
10162
10199
  /** Обменять код на токен. Код одноразовый. */
10163
- complete(code2, deviceName) {
10200
+ complete(code3, deviceName) {
10164
10201
  this.sweep();
10165
- const pending = this.pending.get(code2);
10202
+ const pending = this.pending.get(code3);
10166
10203
  if (!pending) return null;
10167
- this.pending.delete(code2);
10204
+ this.pending.delete(code3);
10168
10205
  const token = generateToken();
10169
10206
  const device = this.runtime.store.pairDevice({
10170
10207
  name: deviceName || pending.name,
@@ -10177,20 +10214,20 @@ var PairingService = class {
10177
10214
  }
10178
10215
  sweep() {
10179
10216
  const now = Date.now();
10180
- for (const [code2, pending] of this.pending) {
10181
- if (pending.expiresAt < now) this.pending.delete(code2);
10217
+ for (const [code3, pending] of this.pending) {
10218
+ if (pending.expiresAt < now) this.pending.delete(code3);
10182
10219
  }
10183
10220
  }
10184
10221
  };
10185
10222
  function generateCode() {
10186
10223
  const alphabet = "ACDEFGHJKLMNPQRTUVWXY34679";
10187
10224
  const bytes = crypto3.randomBytes(8);
10188
- let code2 = "";
10225
+ let code3 = "";
10189
10226
  for (let i = 0; i < 8; i++) {
10190
- if (i === 4) code2 += "-";
10191
- code2 += alphabet[bytes[i] % alphabet.length];
10227
+ if (i === 4) code3 += "-";
10228
+ code3 += alphabet[bytes[i] % alphabet.length];
10192
10229
  }
10193
- return code2;
10230
+ return code3;
10194
10231
  }
10195
10232
  function authenticate(runtime, token) {
10196
10233
  if (!token) return null;
@@ -10247,9 +10284,9 @@ var POLL_TIMEOUT_SECONDS = 25;
10247
10284
  var TelegramError = class extends Error {
10248
10285
  code;
10249
10286
  retryAfter;
10250
- constructor(message, code2, retryAfter2) {
10287
+ constructor(message, code3, retryAfter2) {
10251
10288
  super(message);
10252
- this.code = code2;
10289
+ this.code = code3;
10253
10290
  this.retryAfter = retryAfter2;
10254
10291
  this.name = "TelegramError";
10255
10292
  }
@@ -10384,8 +10421,8 @@ function toTelegramHtml(markdown) {
10384
10421
  return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
10385
10422
  }
10386
10423
  function closeFence(body, language) {
10387
- const code2 = escape(body.join("\n"));
10388
- return language ? `<pre><code class="language-${escape(language)}">${code2}</code></pre>` : `<pre>${code2}</pre>`;
10424
+ const code3 = escape(body.join("\n"));
10425
+ return language ? `<pre><code class="language-${escape(language)}">${code3}</code></pre>` : `<pre>${code3}</pre>`;
10389
10426
  }
10390
10427
  function block(line) {
10391
10428
  const heading = /^\s{0,3}(#{1,6})\s+(.*)$/.exec(line);
@@ -10405,10 +10442,10 @@ function block(line) {
10405
10442
  return inline(line);
10406
10443
  }
10407
10444
  function inline(text) {
10408
- const code2 = [];
10445
+ const code3 = [];
10409
10446
  let work = text.replace(/`([^`]+)`/g, (_, body) => {
10410
- code2.push(`<code>${escape(body)}</code>`);
10411
- return `\0${code2.length - 1}\0`;
10447
+ code3.push(`<code>${escape(body)}</code>`);
10448
+ return `\0${code3.length - 1}\0`;
10412
10449
  });
10413
10450
  work = escape(work);
10414
10451
  work = work.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
@@ -10419,7 +10456,7 @@ function inline(text) {
10419
10456
  work = work.replace(/(^|[\s(])\*([^*\n]+)\*/g, "$1<i>$2</i>");
10420
10457
  work = work.replace(/(^|[\s(])_([^_\n]+)_/g, "$1<i>$2</i>");
10421
10458
  work = work.replace(/~~([^~]+)~~/g, "<s>$1</s>");
10422
- return work.replace(/\u0000(\d+)\u0000/g, (_, index) => code2[Number(index)] ?? "");
10459
+ return work.replace(/\u0000(\d+)\u0000/g, (_, index) => code3[Number(index)] ?? "");
10423
10460
  }
10424
10461
  function escape(text) {
10425
10462
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -10558,17 +10595,17 @@ var TelegramAdapter = class {
10558
10595
  }
10559
10596
  /** Привязка: `/start КОД` меняет код на устройство, как это делает десктоп. */
10560
10597
  async onStart(message, text) {
10561
- const code2 = text.slice("/start".length).trim();
10598
+ const code3 = text.slice("/start".length).trim();
10562
10599
  if (this.bindingOf(message.chat.id)) {
10563
10600
  await this.api.sendMessage(message.chat.id, "\u0423\u0436\u0435 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438. \u041F\u0438\u0448\u0438\u0442\u0435.");
10564
10601
  return;
10565
10602
  }
10566
- if (!code2) {
10603
+ if (!code3) {
10567
10604
  await this.api.sendMessage(message.chat.id, "\u041D\u0443\u0436\u0435\u043D \u043A\u043E\u0434 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F.\n\n\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 Axon \u2192 \xAB\u0423\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430\xBB \u2192 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u043E\u0434, \u043F\u043E\u0442\u043E\u043C \u0441\u044E\u0434\u0430: <code>/start \u041A\u041E\u0414</code>", { parseMode: "HTML" });
10568
10605
  return;
10569
10606
  }
10570
10607
  const name = [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ");
10571
- const paired = this.deps.pair(code2.toUpperCase(), `\u0422\u0435\u043B\u0435\u0433\u0440\u0430\u043C: ${name || message.chat.id}`);
10608
+ const paired = this.deps.pair(code3.toUpperCase(), `\u0422\u0435\u043B\u0435\u0433\u0440\u0430\u043C: ${name || message.chat.id}`);
10572
10609
  if (!paired) {
10573
10610
  await this.api.sendMessage(message.chat.id, "\u041A\u043E\u0434 \u043D\u0435 \u043F\u043E\u0434\u043E\u0448\u0451\u043B \u2014 \u0438\u0441\u0442\u0451\u043A \u0438\u043B\u0438 \u0443\u0436\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D.");
10574
10611
  return;
@@ -11144,13 +11181,13 @@ var UserbotAuth = class {
11144
11181
  * продолжение, и обращаться с ней как с отказом значит терять начатый вход
11145
11182
  * там, где он идёт нормально.
11146
11183
  */
11147
- async signIn(code2) {
11184
+ async signIn(code3) {
11148
11185
  const pending = this.require();
11149
11186
  try {
11150
11187
  await pending.client.invoke(new Api2.auth.SignIn({
11151
11188
  phoneNumber: pending.phone,
11152
11189
  phoneCodeHash: pending.phoneCodeHash,
11153
- phoneCode: code2.trim()
11190
+ phoneCode: code3.trim()
11154
11191
  }));
11155
11192
  } catch (error) {
11156
11193
  if (!isPasswordNeeded(error))
@@ -11199,9 +11236,9 @@ function isPasswordNeeded(error) {
11199
11236
 
11200
11237
  // src/commands.ts
11201
11238
  var CommandError = class extends Error {
11202
- constructor(code2, message) {
11239
+ constructor(code3, message) {
11203
11240
  super(message);
11204
- this.code = code2;
11241
+ this.code = code3;
11205
11242
  this.name = "CommandError";
11206
11243
  }
11207
11244
  };
@@ -11433,8 +11470,8 @@ var handlers = {
11433
11470
  try {
11434
11471
  return { plugin: await runtime.plugins.update(req.id) };
11435
11472
  } catch (error) {
11436
- const code2 = error.code;
11437
- throw new CommandError(code2 === "not_found" ? "not_found" : "bad_request", error.message);
11473
+ const code3 = error.code;
11474
+ throw new CommandError(code3 === "not_found" ? "not_found" : "bad_request", error.message);
11438
11475
  }
11439
11476
  },
11440
11477
  "plugin.configure": async (req, { runtime }) => {
@@ -11555,8 +11592,8 @@ async function withPlugin(run2) {
11555
11592
  try {
11556
11593
  await run2();
11557
11594
  } catch (error) {
11558
- const code2 = error.code;
11559
- throw new CommandError(code2 === "not_found" ? "not_found" : "bad_request", error.message);
11595
+ const code3 = error.code;
11596
+ throw new CommandError(code3 === "not_found" ? "not_found" : "bad_request", error.message);
11560
11597
  }
11561
11598
  }
11562
11599
  function requireConversation(runtime, id) {
@@ -11689,7 +11726,7 @@ var WsSession = class {
11689
11726
  };
11690
11727
 
11691
11728
  // src/Daemon.ts
11692
- var DAEMON_VERSION = true ? "2026.8.25" : "0.0.0-dev";
11729
+ var DAEMON_VERSION = true ? "2026.8.27" : "0.0.0-dev";
11693
11730
  var MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
11694
11731
  function isWildcard(host) {
11695
11732
  return host === "0.0.0.0" || host === "::" || host === "";
@@ -11764,6 +11801,11 @@ var Daemon = class {
11764
11801
  * ответ, и мог бы показать его только целиком в самом конце.
11765
11802
  */
11766
11803
  signalListeners = /* @__PURE__ */ new Set();
11804
+ /**
11805
+ * Пропуск для того, кто сидит за этой машиной: им подписан запрос на новый
11806
+ * код подключения. Живёт в файле рядом с базой, с правами 0600.
11807
+ */
11808
+ controlToken = crypto4.randomBytes(24).toString("hex");
11767
11809
  /** Разрешается, когда все установленные плагины отработали запуск. */
11768
11810
  pluginsReady = Promise.resolve();
11769
11811
  async start() {
@@ -11785,6 +11827,7 @@ var Daemon = class {
11785
11827
  reachable: isWildcard(host) ? reachableUrls(boundPort) : []
11786
11828
  };
11787
11829
  const bootstrapCode = this.pairing.ensureBootstrapCode();
11830
+ this.writeControlToken();
11788
11831
  this.announce();
11789
11832
  logger.info({ url: this.address.url, coreId: this.runtime.coreId }, "\u0434\u0435\u043C\u043E\u043D \u0441\u043B\u0443\u0448\u0430\u0435\u0442");
11790
11833
  this.pluginsReady = this.runtime.startPlugins().catch((error) => {
@@ -11818,7 +11861,7 @@ var Daemon = class {
11818
11861
  const adapter = new TelegramAdapter(
11819
11862
  {
11820
11863
  runtime: this.runtime,
11821
- pair: (code2, name) => this.pairing.complete(code2, name),
11864
+ pair: (code3, name) => this.pairing.complete(code3, name),
11822
11865
  resolvePermission: (requestId, allow) => void this.permissions.resolve(requestId, allow ? "allow_once" : "deny_once"),
11823
11866
  onSignal: (listener) => {
11824
11867
  this.signalListeners.add(listener);
@@ -11903,6 +11946,26 @@ var Daemon = class {
11903
11946
  * же машине мог его найти, не завися от того, кто его поднял, адрес и pid
11904
11947
  * кладутся в файл рядом с базой.
11905
11948
  */
11949
+ controlPath() {
11950
+ return path18.join(this.runtime.config.dataDir, "control.token");
11951
+ }
11952
+ /**
11953
+ * Пропуск переписывается на каждом запуске: старый файл, оставшийся от
11954
+ * убитого процесса, не должен открывать двери в новое ядро.
11955
+ */
11956
+ writeControlToken() {
11957
+ fs18.mkdirSync(this.runtime.config.dataDir, { recursive: true });
11958
+ fs18.writeFileSync(this.controlPath(), `${this.controlToken}
11959
+ `, { mode: 384 });
11960
+ }
11961
+ /** Сравнение постоянного времени: пропуск подбирают по одному символу. */
11962
+ controlAllowed(given) {
11963
+ const value = Array.isArray(given) ? given[0] : given;
11964
+ if (!value) return false;
11965
+ const a = Buffer.from(value.trim());
11966
+ const b = Buffer.from(this.controlToken);
11967
+ return a.length === b.length && crypto4.timingSafeEqual(a, b);
11968
+ }
11906
11969
  announce() {
11907
11970
  const file = path18.join(this.runtime.config.dataDir, "core.json");
11908
11971
  fs18.writeFileSync(
@@ -11952,6 +12015,17 @@ var Daemon = class {
11952
12015
  // ─── HTTP ─────────────────────────────────────────────────────────────────
11953
12016
  async onRequest(req, res) {
11954
12017
  const url = new URL(req.url ?? "/", "http://localhost");
12018
+ if (url.pathname.startsWith("/v1/blobs")) {
12019
+ res.setHeader("Access-Control-Allow-Origin", "*");
12020
+ res.setHeader("Access-Control-Allow-Headers", "authorization, content-type");
12021
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
12022
+ res.setHeader("Access-Control-Max-Age", "86400");
12023
+ if (req.method === "OPTIONS") {
12024
+ res.writeHead(204);
12025
+ res.end();
12026
+ return;
12027
+ }
12028
+ }
11955
12029
  if (req.method === "GET" && url.pathname === "/health") {
11956
12030
  return respond(res, 200, {
11957
12031
  ok: true,
@@ -11962,11 +12036,34 @@ var Daemon = class {
11962
12036
  pendingPermissions: this.permissions.pendingCount
11963
12037
  });
11964
12038
  }
12039
+ if (req.method === "POST" && url.pathname === "/v1/control/pair") {
12040
+ if (!this.controlAllowed(req.headers["x-axon-control"])) {
12041
+ return respond(res, 403, { error: "forbidden" });
12042
+ }
12043
+ const body = await readJson(req);
12044
+ const chatOnly = body?.["scopes"] === "chat";
12045
+ const ttl = Number(body?.["ttlSeconds"] ?? 300);
12046
+ const issued = this.pairing.begin({
12047
+ name: typeof body?.["name"] === "string" ? body["name"] : "\u041D\u043E\u0432\u043E\u0435 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E",
12048
+ platform: "desktop",
12049
+ scopes: chatOnly ? ["chat.read", "chat.write", "tools.safe"] : [
12050
+ "chat.read",
12051
+ "chat.write",
12052
+ "tools.safe",
12053
+ "tools.sensitive",
12054
+ "tools.dangerous",
12055
+ "settings.write",
12056
+ "devices.manage"
12057
+ ],
12058
+ ttlSeconds: Number.isFinite(ttl) && ttl > 0 ? Math.min(ttl, 3600) : 300
12059
+ });
12060
+ return respond(res, 200, issued);
12061
+ }
11965
12062
  if (req.method === "POST" && url.pathname === "/v1/pair") {
11966
12063
  const body = await readJson(req);
11967
- const code2 = typeof body?.["code"] === "string" ? body["code"] : "";
12064
+ const code3 = typeof body?.["code"] === "string" ? body["code"] : "";
11968
12065
  const name = typeof body?.["name"] === "string" ? body["name"] : void 0;
11969
- const paired = this.pairing.complete(code2.trim().toUpperCase(), name);
12066
+ const paired = this.pairing.complete(code3.trim().toUpperCase(), name);
11970
12067
  if (!paired) return respond(res, 403, { error: "invalid_code" });
11971
12068
  return respond(res, 200, {
11972
12069
  token: paired.token,
@@ -12093,6 +12190,8 @@ async function main() {
12093
12190
  return await restore(args);
12094
12191
  case "status":
12095
12192
  return await status();
12193
+ case "code":
12194
+ return await code2(args);
12096
12195
  case "stop":
12097
12196
  return stop();
12098
12197
  case "help":
@@ -12129,11 +12228,11 @@ async function start(args) {
12129
12228
  try {
12130
12229
  started = await daemon.start();
12131
12230
  } catch (error) {
12132
- const code2 = error.code;
12133
- if (code2 === "EADDRINUSE") {
12231
+ const code3 = error.code;
12232
+ if (code3 === "EADDRINUSE") {
12134
12233
  return fail(`\u041F\u043E\u0440\u0442 ${port} \u0437\u0430\u043D\u044F\u0442. \u041C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C, \u044F\u0434\u0440\u043E \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: axon status`);
12135
12234
  }
12136
- if (code2 === "EADDRNOTAVAIL") {
12235
+ if (code3 === "EADDRNOTAVAIL") {
12137
12236
  return fail(
12138
12237
  `\u0410\u0434\u0440\u0435\u0441 ${host} \u043D\u0435 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435.
12139
12238
  \u0427\u0442\u043E\u0431\u044B \u043F\u0443\u0441\u043A\u0430\u0442\u044C \u0434\u0440\u0443\u0433\u0438\u0435 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430, \u0441\u043B\u0443\u0448\u0430\u044E\u0442 \u0432\u0441\u0435 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044B: axon start --host 0.0.0.0`
@@ -12164,6 +12263,37 @@ async function start(args) {
12164
12263
  process.on("SIGINT", stop2);
12165
12264
  process.on("SIGTERM", stop2);
12166
12265
  }
12266
+ async function code2(args) {
12267
+ const record = readRecord();
12268
+ if (!record || !await alive(record.url)) {
12269
+ return fail("\u042F\u0434\u0440\u043E \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u2014 \u043A\u043E\u0434 \u0432\u044B\u0434\u0430\u0451\u0442 \u043E\u043D\u043E \u0441\u0430\u043C\u043E. \u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435: axon start");
12270
+ }
12271
+ const file = path19.join(resolveConfig().dataDir, "control.token");
12272
+ let token;
12273
+ try {
12274
+ token = fs19.readFileSync(file, "utf8").trim();
12275
+ } catch {
12276
+ return fail(`\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A ${file} \u2014 \u043A\u043E\u0434 \u043C\u043E\u0436\u0435\u0442 \u0432\u044B\u0434\u0430\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0445\u043E\u0437\u044F\u0438\u043D \u044F\u0434\u0440\u0430`);
12277
+ }
12278
+ const chatOnly = args.includes("--chat");
12279
+ const ttl = Number(valueOf(args, "--ttl") ?? 300);
12280
+ const response = await fetch(`${record.url}/v1/control/pair`, {
12281
+ method: "POST",
12282
+ headers: { "Content-Type": "application/json", "x-axon-control": token },
12283
+ body: JSON.stringify({ scopes: chatOnly ? "chat" : "full", ttlSeconds: ttl })
12284
+ }).catch(() => null);
12285
+ if (!response?.ok) {
12286
+ return fail(
12287
+ response?.status === 403 ? "\u042F\u0434\u0440\u043E \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B\u043E \u043F\u0440\u043E\u043F\u0443\u0441\u043A. \u041F\u043E\u0445\u043E\u0436\u0435, \u043E\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u043B\u043E\u0441\u044C \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437" : "\u042F\u0434\u0440\u043E \u043D\u0435 \u043E\u0442\u0434\u0430\u043B\u043E \u043A\u043E\u0434"
12288
+ );
12289
+ }
12290
+ const issued = await response.json();
12291
+ console.log(issued.code);
12292
+ console.log("");
12293
+ console.log(` \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 ${Math.round(issued.expiresInSeconds / 60)} \u043C\u0438\u043D, \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u044B\u0439`);
12294
+ console.log(` \u043F\u0440\u0430\u0432\u0430: ${chatOnly ? "\u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0435\u043F\u0438\u0441\u043A\u0430" : "\u043F\u043E\u043B\u043D\u044B\u0435"}`);
12295
+ console.log(` \u0430\u0434\u0440\u0435\u0441 \u0434\u043B\u044F \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F: ${(record.reachable ?? [])[0] ?? record.url}`);
12296
+ }
12167
12297
  function logPath() {
12168
12298
  return path19.join(resolveConfig().dataDir, "axon.log");
12169
12299
  }
@@ -12190,11 +12320,11 @@ async function detach(args) {
12190
12320
  console.log(` \u0432\u044B\u0432\u043E\u0434: ${file}`);
12191
12321
  console.log(" \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C: axon stop");
12192
12322
  const codeFile = path19.join(config.dataDir, "bootstrap.code");
12193
- const code2 = fs19.existsSync(codeFile) ? fs19.readFileSync(codeFile, "utf8").trim() : "";
12194
- if (code2) {
12323
+ const code3 = fs19.existsSync(codeFile) ? fs19.readFileSync(codeFile, "utf8").trim() : "";
12324
+ if (code3) {
12195
12325
  console.log("");
12196
12326
  console.log(" \u0423\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442. \u041A\u043E\u0434 \u0434\u043B\u044F \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F:");
12197
- console.log(` ${code2}`);
12327
+ console.log(` ${code3}`);
12198
12328
  console.log("");
12199
12329
  }
12200
12330
  }
@@ -12460,6 +12590,7 @@ function usage() {
12460
12590
  --host 0.0.0.0 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0434\u043B\u044F \u0434\u0440\u0443\u0433\u0438\u0445 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432
12461
12591
  --foreground, -f \u043D\u0435 \u0443\u0445\u043E\u0434\u0438\u0442\u044C \u0432 \u0444\u043E\u043D (systemd, docker)
12462
12592
  axon status \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u043B\u0438 \u044F\u0434\u0440\u043E \u0438 \u0433\u0434\u0435
12593
+ axon code [--chat] [--ttl 300] \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u0434 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430
12463
12594
  axon stop \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u044F\u0434\u0440\u043E
12464
12595
  axon secret list \u043A\u0430\u043A\u0438\u0435 \u0441\u0435\u043A\u0440\u0435\u0442\u044B \u0437\u0430\u0434\u0430\u043D\u044B
12465
12596
  axon secret get <\u043A\u043B\u044E\u0447> \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0441\u0435\u043A\u0440\u0435\u0442 \u0446\u0435\u043B\u0438\u043A\u043E\u043C
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/Daemon.ts
2
+ import crypto4 from "node:crypto";
2
3
  import http from "node:http";
3
4
  import fs16 from "node:fs";
4
5
  import os7 from "node:os";
@@ -680,10 +681,33 @@ var SecretStore = class {
680
681
  const row = this.db.prepare(`SELECT * FROM secrets WHERE key = ?`).get(key);
681
682
  if (!row)
682
683
  return null;
684
+ return this.decrypt(row);
685
+ }
686
+ /**
687
+ * Расшифровать строку. Бросает, если значение не сходится с ключом.
688
+ *
689
+ * GCM проверяет подпись, поэтому чужой ключ шифрования — это не мусор на
690
+ * выходе, а честный отказ. Ловить его здесь нельзя: молча вернуть `null`
691
+ * значит превратить «ключ не читается» в «ключа нет», а лечатся они
692
+ * по-разному.
693
+ */
694
+ decrypt(row) {
683
695
  const decipher = crypto.createDecipheriv(ALGORITHM, this.masterKey(), row.iv);
684
696
  decipher.setAuthTag(row.tag);
685
697
  return Buffer.concat([decipher.update(row.ciphertext), decipher.final()]).toString("utf8");
686
698
  }
699
+ /** Читается ли значение здешним ключом шифрования. */
700
+ readable(key) {
701
+ const row = this.db.prepare(`SELECT * FROM secrets WHERE key = ?`).get(key);
702
+ if (!row)
703
+ return false;
704
+ try {
705
+ this.decrypt(row);
706
+ return true;
707
+ } catch {
708
+ return false;
709
+ }
710
+ }
687
711
  delete(key) {
688
712
  this.db.prepare(`DELETE FROM secrets WHERE key = ?`).run(key);
689
713
  }
@@ -704,7 +728,12 @@ var SecretStore = class {
704
728
  key,
705
729
  set: true,
706
730
  ...row.hint ? { hint: row.hint } : {},
707
- updatedAt: row.updated_at
731
+ updatedAt: row.updated_at,
732
+ // Наличие строки в базе — ещё не рабочий секрет: база могла приехать
733
+ // с другой машины, а ключ шифрования остаться там. «Ключ на месте» в
734
+ // таком случае врало, и человек искал причину в чём угодно, кроме
735
+ // переноса.
736
+ ...this.readable(key) ? {} : { unreadable: true }
708
737
  };
709
738
  });
710
739
  }
@@ -1724,7 +1753,7 @@ import fs14 from "node:fs";
1724
1753
  // ../protocol/dist/primitives.js
1725
1754
  import { z } from "zod";
1726
1755
  var PROTOCOL_VERSION = 1;
1727
- var PROTOCOL_REVISION = 10;
1756
+ var PROTOCOL_REVISION = 11;
1728
1757
  var zTimestamp = z.string().datetime();
1729
1758
  var zId = z.string().uuid();
1730
1759
  var zExternalId = z.string().min(1).max(256);
@@ -2698,7 +2727,15 @@ var zSecretStatus = z10.object({
2698
2727
  set: z10.boolean(),
2699
2728
  /** Последние символы значения. Присутствует, только если секрет задан. */
2700
2729
  hint: z10.string().max(8).optional(),
2701
- updatedAt: zTimestamp.optional()
2730
+ updatedAt: zTimestamp.optional(),
2731
+ /**
2732
+ * Значение лежит, но расшифровать его нечем.
2733
+ *
2734
+ * Так бывает после переноса: база приехала, а ключ шифрования рядом с ней —
2735
+ * нет (`axon backup` не кладёт его в архив без спроса). Снаружи это выглядело
2736
+ * как «ключ на месте», хотя ни один запрос с ним не проходит.
2737
+ */
2738
+ unreadable: z10.boolean().optional()
2702
2739
  });
2703
2740
  var zSettingsGetReq = z10.object({
2704
2741
  /** Какие ключи вернуть. Пусто — все, к которым у устройства есть доступ. */
@@ -11352,7 +11389,7 @@ var WsSession = class {
11352
11389
  };
11353
11390
 
11354
11391
  // src/Daemon.ts
11355
- var DAEMON_VERSION = true ? "2026.8.25" : "0.0.0-dev";
11392
+ var DAEMON_VERSION = true ? "2026.8.27" : "0.0.0-dev";
11356
11393
  var MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
11357
11394
  function isWildcard(host) {
11358
11395
  return host === "0.0.0.0" || host === "::" || host === "";
@@ -11427,6 +11464,11 @@ var Daemon = class {
11427
11464
  * ответ, и мог бы показать его только целиком в самом конце.
11428
11465
  */
11429
11466
  signalListeners = /* @__PURE__ */ new Set();
11467
+ /**
11468
+ * Пропуск для того, кто сидит за этой машиной: им подписан запрос на новый
11469
+ * код подключения. Живёт в файле рядом с базой, с правами 0600.
11470
+ */
11471
+ controlToken = crypto4.randomBytes(24).toString("hex");
11430
11472
  /** Разрешается, когда все установленные плагины отработали запуск. */
11431
11473
  pluginsReady = Promise.resolve();
11432
11474
  async start() {
@@ -11448,6 +11490,7 @@ var Daemon = class {
11448
11490
  reachable: isWildcard(host) ? reachableUrls(boundPort) : []
11449
11491
  };
11450
11492
  const bootstrapCode = this.pairing.ensureBootstrapCode();
11493
+ this.writeControlToken();
11451
11494
  this.announce();
11452
11495
  logger.info({ url: this.address.url, coreId: this.runtime.coreId }, "\u0434\u0435\u043C\u043E\u043D \u0441\u043B\u0443\u0448\u0430\u0435\u0442");
11453
11496
  this.pluginsReady = this.runtime.startPlugins().catch((error) => {
@@ -11566,6 +11609,26 @@ var Daemon = class {
11566
11609
  * же машине мог его найти, не завися от того, кто его поднял, адрес и pid
11567
11610
  * кладутся в файл рядом с базой.
11568
11611
  */
11612
+ controlPath() {
11613
+ return path16.join(this.runtime.config.dataDir, "control.token");
11614
+ }
11615
+ /**
11616
+ * Пропуск переписывается на каждом запуске: старый файл, оставшийся от
11617
+ * убитого процесса, не должен открывать двери в новое ядро.
11618
+ */
11619
+ writeControlToken() {
11620
+ fs16.mkdirSync(this.runtime.config.dataDir, { recursive: true });
11621
+ fs16.writeFileSync(this.controlPath(), `${this.controlToken}
11622
+ `, { mode: 384 });
11623
+ }
11624
+ /** Сравнение постоянного времени: пропуск подбирают по одному символу. */
11625
+ controlAllowed(given) {
11626
+ const value = Array.isArray(given) ? given[0] : given;
11627
+ if (!value) return false;
11628
+ const a = Buffer.from(value.trim());
11629
+ const b = Buffer.from(this.controlToken);
11630
+ return a.length === b.length && crypto4.timingSafeEqual(a, b);
11631
+ }
11569
11632
  announce() {
11570
11633
  const file = path16.join(this.runtime.config.dataDir, "core.json");
11571
11634
  fs16.writeFileSync(
@@ -11615,6 +11678,17 @@ var Daemon = class {
11615
11678
  // ─── HTTP ─────────────────────────────────────────────────────────────────
11616
11679
  async onRequest(req, res) {
11617
11680
  const url = new URL(req.url ?? "/", "http://localhost");
11681
+ if (url.pathname.startsWith("/v1/blobs")) {
11682
+ res.setHeader("Access-Control-Allow-Origin", "*");
11683
+ res.setHeader("Access-Control-Allow-Headers", "authorization, content-type");
11684
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
11685
+ res.setHeader("Access-Control-Max-Age", "86400");
11686
+ if (req.method === "OPTIONS") {
11687
+ res.writeHead(204);
11688
+ res.end();
11689
+ return;
11690
+ }
11691
+ }
11618
11692
  if (req.method === "GET" && url.pathname === "/health") {
11619
11693
  return respond(res, 200, {
11620
11694
  ok: true,
@@ -11625,6 +11699,29 @@ var Daemon = class {
11625
11699
  pendingPermissions: this.permissions.pendingCount
11626
11700
  });
11627
11701
  }
11702
+ if (req.method === "POST" && url.pathname === "/v1/control/pair") {
11703
+ if (!this.controlAllowed(req.headers["x-axon-control"])) {
11704
+ return respond(res, 403, { error: "forbidden" });
11705
+ }
11706
+ const body = await readJson(req);
11707
+ const chatOnly = body?.["scopes"] === "chat";
11708
+ const ttl = Number(body?.["ttlSeconds"] ?? 300);
11709
+ const issued = this.pairing.begin({
11710
+ name: typeof body?.["name"] === "string" ? body["name"] : "\u041D\u043E\u0432\u043E\u0435 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E",
11711
+ platform: "desktop",
11712
+ scopes: chatOnly ? ["chat.read", "chat.write", "tools.safe"] : [
11713
+ "chat.read",
11714
+ "chat.write",
11715
+ "tools.safe",
11716
+ "tools.sensitive",
11717
+ "tools.dangerous",
11718
+ "settings.write",
11719
+ "devices.manage"
11720
+ ],
11721
+ ttlSeconds: Number.isFinite(ttl) && ttl > 0 ? Math.min(ttl, 3600) : 300
11722
+ });
11723
+ return respond(res, 200, issued);
11724
+ }
11628
11725
  if (req.method === "POST" && url.pathname === "/v1/pair") {
11629
11726
  const body = await readJson(req);
11630
11727
  const code = typeof body?.["code"] === "string" ? body["code"] : "";
@@ -986,7 +986,15 @@ var zSecretStatus = z10.object({
986
986
  set: z10.boolean(),
987
987
  /** Последние символы значения. Присутствует, только если секрет задан. */
988
988
  hint: z10.string().max(8).optional(),
989
- updatedAt: zTimestamp.optional()
989
+ updatedAt: zTimestamp.optional(),
990
+ /**
991
+ * Значение лежит, но расшифровать его нечем.
992
+ *
993
+ * Так бывает после переноса: база приехала, а ключ шифрования рядом с ней —
994
+ * нет (`axon backup` не кладёт его в архив без спроса). Снаружи это выглядело
995
+ * как «ключ на месте», хотя ни один запрос с ним не проходит.
996
+ */
997
+ unreadable: z10.boolean().optional()
990
998
  });
991
999
  var zSettingsGetReq = z10.object({
992
1000
  /** Какие ключи вернуть. Пусто — все, к которым у устройства есть доступ. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axon-assistant/core",
3
- "version": "2026.8.25",
3
+ "version": "2026.8.27",
4
4
  "comment:version": "Заглушка. Настоящий номер проставляется из git-тега при публикации: см. scripts/release-npm.mjs. Версия, которую правят руками, рано или поздно расходится с тем, что опубликовано.",
5
5
  "description": "Ядро Axon: персональный AI-агент, который живёт на вашей машине",
6
6
  "keywords": [