claude-telegram-mirror 0.2.23 → 0.2.25

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
@@ -137,20 +137,33 @@ Approval buttons only appear in normal mode, not with `--dangerously-skip-permis
137
137
  ## Architecture
138
138
 
139
139
  ```
140
- ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
141
- │ Claude Code │────▶│ ctm daemon │────▶│ Telegram │
142
- │ CLI │◀────│ (Rust binary) │◀────│ Bot
143
- └─────────────────┘ └─────────────────┘ └─────────────────┘
144
-
145
- │ hooks │ Unix socket
146
- ▼ ▼
147
- ┌─────────────────┐ ┌─────────────────┐
148
- │ ctm hook │────▶│ Socket Server │
149
- (same binary, │◀────│ (bidirectional)│
150
- hook mode)
151
- └─────────────────┘ └─────────────────┘
140
+ OUTBOUND (CLI → Telegram) — Claude Code mirrors all activity out:
141
+
142
+ ┌─────────────┐ fires ┌──────────┐ NDJSON over ┌──────────────┐ Bot API ┌──────────────┐
143
+ │ Claude Code │ hook │ ctm hook │ Unix socket │ ctm daemon │ sendMsg │ Telegram │
144
+ CLI (tmux) ───────▶ │ (binary) │ ────────────▶ │ (event loop) │ ─────────▶ │ forum topic │
145
+ └─────────────┘ └──────────┘ └──────────────┘ └──────────────┘
146
+
147
+ INBOUND (Telegram → CLI) — daemon injects into the live pane:
148
+
149
+ ┌─────────────┐ tmux send-keys ┌──────────────┐ getUpdates long poll ┌──────────────┐
150
+ Claude Code ◀─────────────── ctm daemon ◀─────────────────────── │ Telegram │
151
+ │ CLI (tmux) │ -t <pane> │ InputInjector│ text & button callbacks │ forum topic │
152
+ └─────────────┘ └──────────────┘ └──────────────┘
153
+
154
+ Approvals (PreToolUse): the hook blocks on the Unix socket for an approval_response;
155
+ the daemon shows inline buttons in Telegram and writes the verdict back to the hook.
156
+
157
+ ctm daemon = tokio event loop · Unix SocketServer · SessionManager (SQLite) + per-session tmux cache
158
+ · TelegramBot (Bot API) · InputInjector (tmux) · pending approval / question state
152
159
  ```
153
160
 
161
+ > The `ctm hook` and the daemon are the **same binary** in different modes. Outbound rides
162
+ > hook → Unix socket → daemon → Bot API; inbound rides daemon long-poll → `InputInjector`
163
+ > (`tmux send-keys`) into the live CLI pane. The daemon resolves a session's tmux pane from
164
+ > its cache then SQLite, and **fails closed** if that session never reported one (ROUTING-001) —
165
+ > it never guesses a pane, to avoid misrouting keystrokes into another session.
166
+
154
167
  **Flow:**
155
168
  1. Claude Code hooks invoke `ctm hook`, which reads the event from stdin
156
169
  2. PreToolUse: for tool approvals, sends an approval request via socket and blocks for the Telegram response. **AskUserQuestion is *not* intercepted here** — the hook returns fast so Claude renders its native widget in the CLI; the daemon mirrors the question to Telegram from the standard `tool_start` event (ADR-015)
@@ -301,7 +314,7 @@ ctm install-hooks --project
301
314
  - **Topic routing**: Each daemon only processes topics it created (multi-bot safe)
302
315
  - **Rate limiting**: Governor-based with exponential backoff retry queue
303
316
  - **Token scrubbing**: All log output filtered through regex to strip bot tokens
304
- - **Test suite**: 512 Rust tests (unit + 10 integration test files)
317
+ - **Test suite**: 470+ Rust tests (unit + 11 integration test files)
305
318
 
306
319
  ## Troubleshooting
307
320
 
@@ -367,7 +380,7 @@ cargo test
367
380
  ./target/release/ctm start
368
381
  ```
369
382
 
370
- ### Project Structure (30 source files)
383
+ ### Project Structure (33 source files)
371
384
 
372
385
  ```
373
386
  rust-crates/ctm/src/
@@ -382,16 +395,19 @@ rust-crates/ctm/src/
382
395
  injector.rs # tmux input injection
383
396
  formatting.rs # Message formatting, chunking, ANSI stripping
384
397
  summarize.rs # Tool action summarizer (30+ patterns)
398
+ liveness.rs # tmux pane liveness checks for topic reconciliation
399
+ prune.rs # prune-topics subcommand (bulk stale-topic cleanup)
385
400
  colors.rs # ANSI color helpers for terminal output
386
401
  doctor.rs # Diagnostic checks with --fix
387
402
  installer.rs # Hook installer
388
403
  setup.rs # Interactive setup wizard
389
- bot/ # Telegram API client (client.rs, queue.rs, types.rs)
404
+ bot/ # Telegram API client (mod.rs, client.rs, queue.rs, types.rs)
390
405
  daemon/ # Bridge daemon (mod.rs, event_loop.rs, socket_handlers.rs,
391
- # telegram_handlers.rs, callback_handlers.rs, cleanup.rs, files.rs)
406
+ # telegram_handlers.rs, callback_handlers.rs, cleanup.rs,
407
+ # reconcile.rs, files.rs)
392
408
  service/ # OS service management (mod.rs, systemd.rs, launchd.rs, env.rs)
393
409
 
394
- rust-crates/ctm/tests/ # 10 integration test files
410
+ rust-crates/ctm/tests/ # 11 integration test files
395
411
  ```
396
412
 
397
413
  </details>
package/SECURITY.md CHANGED
@@ -62,11 +62,11 @@ metacharacters are rejected.
62
62
 
63
63
  ### 2. Bot Token Scrubbing
64
64
 
65
- **File:** `rust-crates/ctm/src/bot/client.rs`
65
+ **Files:** `rust-crates/ctm/src/main.rs` (subscriber layer), `rust-crates/ctm/src/bot/mod.rs` (regex)
66
66
 
67
- A `tracing` subscriber layer applies `scrub_bot_token()` to log output.
68
- The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token pattern
69
- in API URLs and replaces it with `bot<REDACTED>`.
67
+ A `tracing` subscriber layer (`ScrubWriter` in `main.rs`) applies `scrub_bot_token()`
68
+ to all log output. The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token
69
+ pattern in API URLs and replaces it with `bot[REDACTED]/`.
70
70
 
71
71
  All log output goes to stderr via the `tracing` subscriber. There is no
72
72
  file transport, so tokens cannot leak into log files on disk.
@@ -79,8 +79,9 @@ tokens from error messages before logging.
79
79
  **File:** `rust-crates/ctm/src/daemon/telegram_handlers.rs`
80
80
 
81
81
  A chat ID check verifies `chat.id` against the configured `chat_id` on
82
- every incoming update. Updates from unauthorized chats receive a static
83
- "Unauthorized" reply and are not processed further.
82
+ every incoming update. Updates from unauthorized chats are silently dropped
83
+ (logged as a warning, no reply) by design, since replying would confirm the
84
+ bot's existence and function to an attacker (ADR-006 L4.6).
84
85
 
85
86
  Approval callback handlers (`approve:`, `reject:`, `abort:`), answer
86
87
  handlers (`answer:`, `toggle:`, `submit:`), all verify the chat ID
@@ -94,7 +95,7 @@ different chat.
94
95
 
95
96
  Session IDs from hook events are validated before any database operation:
96
97
  - Maximum length: 128 characters
97
- - Character set: `[a-zA-Z0-9_-]` only
98
+ - Character set: `[a-zA-Z0-9_.-]` only
98
99
  - Empty/null values are rejected
99
100
 
100
101
  Messages with invalid session IDs are dropped with a warning log.
@@ -118,10 +119,10 @@ socket.
118
119
 
119
120
  **File:** `rust-crates/ctm/src/config.rs`
120
121
 
121
- `validateSocketPath()` rejects socket paths that:
122
+ `validate_socket_path()` rejects socket paths that:
122
123
  - Contain `..` (directory traversal)
123
124
  - Are not absolute (do not start with `/`)
124
- - Exceed 256 characters
125
+ - Exceed 104 characters (the AF_UNIX `sun_path` limit)
125
126
 
126
127
  Invalid paths fall back to the default socket path in the config directory.
127
128
 
@@ -151,7 +152,7 @@ handler logs a warning and exits cleanly without processing the payload.
151
152
 
152
153
  ### 10. Download File Handling
153
154
 
154
- **File:** `rust-crates/ctm/src/daemon/telegram_handlers.rs`
155
+ **File:** `rust-crates/ctm/src/daemon/files.rs`
155
156
 
156
157
  Downloaded files from Telegram are handled with several protections:
157
158
  - The downloads directory is created with mode 0o700
@@ -179,11 +180,11 @@ Downloaded files from Telegram are handled with several protections:
179
180
 
180
181
  | Boundary | Validation | Enforcement |
181
182
  |---|---|---|
182
- | Socket messages: session ID | 128 char max, `[a-zA-Z0-9_-]` | `socket_handlers.rs` — `is_valid_session_id()` |
183
+ | Socket messages: session ID | 128 char max, `[a-zA-Z0-9_.-]` | `socket_handlers.rs` — `is_valid_session_id()` |
183
184
  | Socket lines | 1 MiB max per NDJSON line | `socket.rs` — `MAX_LINE_BYTES` |
184
185
  | Socket connections | 64 concurrent max | `socket.rs` — `MAX_CONNECTIONS` |
185
186
  | Hook stdin | 1 MiB max | `hook.rs` — `MAX_STDIN_BYTES` |
186
- | Socket paths | No `..`, absolute only, 256 char max | `config.rs` — `validate_socket_path()` |
187
+ | Socket paths | No `..`, absolute only, 104 char max | `config.rs` — `validate_socket_path()` |
187
188
  | Slash commands | Character whitelist: `[a-zA-Z0-9_- /]` | `injector.rs` — `is_valid_slash_command()` |
188
189
  | Download filenames | Sanitized, UUID-prefixed, no `..`, 200 char max | `telegram_handlers.rs` — `sanitize_filename()` |
189
190
  | Download file size | 20 MB max | Telegram Bot API server-side limit |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-mirror",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Bidirectional Telegram integration for Claude Code CLI - monitor and control your Claude Code sessions from Telegram",
5
5
  "bin": {
6
6
  "claude-telegram-mirror": "./scripts/ctm-wrapper.cjs",
@@ -49,10 +49,10 @@
49
49
  "access": "public"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@agidreams/ctm-linux-x64": "0.2.23",
53
- "@agidreams/ctm-linux-arm64": "0.2.23",
54
- "@agidreams/ctm-darwin-arm64": "0.2.23",
55
- "@agidreams/ctm-darwin-x64": "0.2.23"
52
+ "@agidreams/ctm-linux-x64": "0.2.25",
53
+ "@agidreams/ctm-linux-arm64": "0.2.25",
54
+ "@agidreams/ctm-darwin-arm64": "0.2.25",
55
+ "@agidreams/ctm-darwin-x64": "0.2.25"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=18.0.0"
package/postinstall.cjs CHANGED
@@ -7,6 +7,71 @@
7
7
  const os = require('os');
8
8
  const path = require('path');
9
9
  const fs = require('fs');
10
+ const { execFileSync } = require('child_process');
11
+
12
+ /**
13
+ * macOS hardening: a binary delivered via npm carries only the ad-hoc signature
14
+ * the build toolchain applied — it is NOT Developer-ID signed or notarized. On
15
+ * modern macOS the kernel can SIGKILL such a binary for a code-signing /
16
+ * launch-constraint violation (EXC_CRASH / "Code Signature Invalid"),
17
+ * especially on the first launchd-spawned launch after install. This pass is
18
+ * defense-in-depth: strip any quarantine flag, ensure the binary has a coherent
19
+ * (at least ad-hoc) signature, and smoke-test that it can actually exec. It
20
+ * never throws — install must still succeed — but it warns loudly so the user
21
+ * is not left with a binary the OS silently refuses to run.
22
+ */
23
+ function hardenMacBinary(binary) {
24
+ if (process.platform !== 'darwin') return;
25
+
26
+ const run = (cmd, args) => {
27
+ try {
28
+ return { ok: true, out: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) };
29
+ } catch (e) {
30
+ return { ok: false, out: (e.stdout || '') + (e.stderr || ''), err: e };
31
+ }
32
+ };
33
+
34
+ // 1. Remove the quarantine xattr if a download path applied one. Best-effort.
35
+ run('xattr', ['-d', 'com.apple.quarantine', binary]);
36
+
37
+ // 2. If the signature is missing/invalid, re-apply an ad-hoc signature so the
38
+ // Mach-O is at least internally consistent and execable on Apple Silicon.
39
+ const verify = run('codesign', ['--verify', '--strict', binary]);
40
+ if (!verify.ok) {
41
+ const resign = run('codesign', ['--force', '--sign', '-', binary]);
42
+ if (!resign.ok) {
43
+ console.log('WARNING: Could not re-sign the native binary; macOS may refuse to run it.');
44
+ console.log(' Try manually: codesign --force --sign - "' + binary + '"');
45
+ }
46
+ }
47
+
48
+ // 3. Smoke-test exec. If the OS kills it here, the user would otherwise only
49
+ // discover it when the daemon silently fails to start.
50
+ const smoke = run(binary, ['--version']);
51
+ if (!smoke.ok) {
52
+ console.log('');
53
+ console.log('WARNING: The native ctm binary failed to execute on this machine.');
54
+ console.log(' This is typically a macOS code-signing / Gatekeeper rejection of a');
55
+ console.log(' non-notarized binary. To inspect:');
56
+ console.log(' codesign -dvvv "' + binary + '"');
57
+ console.log(' ls ~/Library/Logs/DiagnosticReports/ctm-*.ips');
58
+ console.log(' Or build from source: cd rust-crates && cargo build --release');
59
+ console.log('');
60
+ }
61
+ }
62
+
63
+ // Protect the native binary on every real install — independent of whether we
64
+ // also print the setup guidance below. Skipped under CI (the binary is built
65
+ // and verified there, not consumed).
66
+ if (!process.env.CI) {
67
+ try {
68
+ const { resolveBinary } = require('./scripts/resolve-binary.cjs');
69
+ const r = resolveBinary();
70
+ if (r) hardenMacBinary(r.binary);
71
+ } catch {
72
+ // resolve-binary unavailable — skip silently
73
+ }
74
+ }
10
75
 
11
76
  // Don't show guidance during CI or if TELEGRAM_BOT_TOKEN is already set
12
77
  if (process.env.CI || process.env.TELEGRAM_BOT_TOKEN) {