@agfpd/iapeer 0.4.23 → 0.4.24

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
@@ -10,6 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.4.24] - 2026-06-22
14
+
15
+ ### Fixed
16
+
17
+ - **`iapeer connect telegram <peer>` (and the `enable telegram` alias) no longer fails for a
18
+ peer whose personality isn't a valid Telegram @username** (e.g. a hyphen: "impact-finder").
19
+ connect passed the PERSONALITY as the bot @username to telegram-runtime (which keys bots by
20
+ `<bot-username>`), so a hyphenated personality was rejected ("must be a telegram @username").
21
+ The bot @username is TOKEN-derived (set in @BotFather), never the personality. connect now
22
+ resolves it from the token via Telegram **getMe** and uses that for `bot add` / `interface
23
+ bot` / storage; the human still owes only the token. An invalid/unreachable token fails
24
+ early with a getMe-rejected message. (The getMe lives in the telegram-glue module
25
+ `connect/index.ts` — the foundation core stays Telegram-API-free.)
26
+
13
27
  ## [0.4.23] - 2026-06-22
14
28
 
15
29
  ### Added
@@ -352,7 +366,8 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
352
366
  - **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
353
367
  (timer = cron, watcher = event) are all first-class and addressed the same way.
354
368
 
355
- [Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.23...HEAD
369
+ [Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.24...HEAD
370
+ [0.4.24]: https://github.com/agfpd/iapeer/compare/v0.4.23...v0.4.24
356
371
  [0.4.23]: https://github.com/agfpd/iapeer/compare/v0.4.22...v0.4.23
357
372
  [0.4.22]: https://github.com/agfpd/iapeer/compare/v0.4.21...v0.4.22
358
373
  [0.4.21]: https://github.com/agfpd/iapeer/compare/v0.4.20...v0.4.21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.23",
3
+ "version": "0.4.24",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -5,9 +5,11 @@
5
5
  //
6
6
  // The human owes EXACTLY ONE external fact: the bot token (prompt walks them
7
7
  // through @BotFather → /newbot). Everything else the system resolves: the bot
8
- // alias (= the peer's personality), `telegram-runtime bot add` (owner adds getMe
9
- // validation — an invalid token fails THERE, early), `telegram-runtime interface
10
- // bot` (profile merge; precondition: the peer is registered), then the MANDATORY
8
+ // @username via Telegram getMe(token) — the bot key is TOKEN-derived, NOT the peer
9
+ // personality (a personality may contain a hyphen, structurally invalid as a
10
+ // @username; telegram-runtime keys bots by <bot-username>), `telegram-runtime bot add`
11
+ // (also validates the token), `telegram-runtime interface bot` (profile merge;
12
+ // precondition: the peer is registered), then the MANDATORY
11
13
  // router-session restart (the live poller reads bots/ ONCE at start, no fs-watch —
12
14
  // without the restart the channel is dead both ways), and the activation hint:
13
15
  // the FIRST message from the human to the bot opens the chat (a Telegram platform
@@ -51,6 +53,9 @@ export interface ConnectTelegramOptions {
51
53
  runTg?: TgRunner
52
54
  /** Injectable router restart (tests). Default: stopPeer→startPeer strictly in order. */
53
55
  restart?: RestartFn
56
+ /** Injectable bot-@username resolver (tests). Default: Telegram getMe(token). Returns the
57
+ * bare @username (no leading @) or null on a rejected/unreachable token. */
58
+ resolveUsername?: (token: string) => Promise<string | null>
54
59
  }
55
60
 
56
61
  export interface ConnectTelegramResult {
@@ -60,6 +65,7 @@ export interface ConnectTelegramResult {
60
65
  | 'refused-no-token' // non-tty and no --token (or an empty tty answer)
61
66
  | 'unregistered-peer' // precondition: the peer must be in the registry
62
67
  | 'runtime-missing' // no telegram runtime manifest — install it first
68
+ | 'bad-token' // Telegram getMe rejected the token (invalid/revoked) or was unreachable
63
69
  | 'bot-add-failed' // telegram-runtime bot add exited non-zero (incl. getMe refusal)
64
70
  | 'interface-failed' // telegram-runtime interface bot exited non-zero
65
71
  peer: string
@@ -93,6 +99,26 @@ function readBotEnv(alias: string, env: NodeJS.ProcessEnv): string | null {
93
99
  }
94
100
  }
95
101
 
102
+ /**
103
+ * Resolve the bot's REAL @username from the token via Telegram getMe — the bot identity
104
+ * is TOKEN-derived (BotFather sets it), NOT the peer personality. telegram-runtime keys
105
+ * bots by `<bot-username>` (`bot add <bot-username>`, `interface bot <bot-username>`), and a
106
+ * personality can contain a hyphen which is structurally invalid as a @username — so the
107
+ * personality can NEVER be the bot-username. Returns the bare username (no leading @) or
108
+ * null on a rejected/unreachable token. The token goes literally in the URL path
109
+ * (Telegram tokens are `<digits>:<urlsafe>`; no encoding).
110
+ */
111
+ async function defaultResolveBotUsername(token: string): Promise<string | null> {
112
+ try {
113
+ const r = await fetch(`https://api.telegram.org/bot${token}/getMe`)
114
+ const j = (await r.json()) as { ok?: boolean; result?: { username?: unknown } }
115
+ const u = j?.ok === true && typeof j.result?.username === 'string' ? j.result.username.trim().replace(/^@/, '') : ''
116
+ return u || null
117
+ } catch {
118
+ return null // network error / non-JSON → treat as unresolvable (bad-token)
119
+ }
120
+ }
121
+
96
122
  /** Resolve the telegram-runtime CLI bin from the manifest's selfConfig command (the
97
123
  * package's own absolute bin — PATH-independent, launchd-safe). */
98
124
  function resolveTgBin(env: NodeJS.ProcessEnv): string | null {
@@ -173,8 +199,17 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
173
199
  if (!token) return { state: 'refused-no-token', peer, detail: 'no answer — nothing connected' }
174
200
  }
175
201
 
202
+ // Resolve the bot's REAL @username from the token (getMe) — NOT the personality. The
203
+ // bot-username is token-derived; a personality with a hyphen (e.g. "impact-finder") is
204
+ // structurally invalid as a @username and telegram-runtime (which keys bots by
205
+ // <bot-username>) rejects it. getMe also validates the token early (bad token → fail here).
206
+ const resolveUsername = opts.resolveUsername ?? defaultResolveBotUsername
207
+ const botUsername = await resolveUsername(token)
208
+ if (!botUsername) {
209
+ return { state: 'bad-token', peer, detail: 'Telegram getMe rejected the token (invalid/revoked) or was unreachable — recheck the token with @BotFather' }
210
+ }
176
211
  const runTg = opts.runTg ?? defaultRunTg(tgBin)
177
- const alias = peer // bot alias = the peer's personality (design §(в); passes the owner's NAME_RE)
212
+ const alias = botUsername // bot key = the REAL @username from getMe (telegram-runtime keys bots by <bot-username>)
178
213
  const before = readBotEnv(alias, env)
179
214
 
180
215
  // (1) bot add — token → bots/<alias>/.env. Owner adds getMe validation here: an
@@ -183,17 +218,10 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
183
218
  if (add.status !== 0) {
184
219
  return { state: 'bot-add-failed', peer, detail: (add.stderr || add.stdout || `exit ${add.status}`).trim() }
185
220
  }
186
- // @username: the bots/<alias>/.env TELEGRAM_BOT_USERNAME field is the RELIABLE
187
- // source (present on the live host; survives a quiet bot-add stdout — acceptance
188
- // testing saw the activation line degrade to the BotFather hint). stdout
189
- // match stays as the fallback.
190
- const envAfterAdd = readBotEnv(alias, env)
191
- const envUser = envAfterAdd?.match(/^TELEGRAM_BOT_USERNAME=(.+)$/m)?.[1]?.trim()
192
- const username = envUser
193
- ? envUser.startsWith('@')
194
- ? envUser
195
- : `@${envUser}`
196
- : add.stdout.match(/@[A-Za-z0-9_]{3,}/)?.[0]
221
+ // @username for display / the activation hint: the bot key (alias) IS the
222
+ // getMe-resolved username, so the display form is simply `@<alias>` — no need to
223
+ // re-parse bots/<alias>/.env (telegram-runtime writes the same value there).
224
+ const username = `@${alias}`
197
225
 
198
226
  // (2) interface bot — merge the channel binding into the peer's profile.
199
227
  const iface = runTg(['interface', 'bot', alias, '--peer', peer], env)