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 +33 -17
- package/SECURITY.md +13 -12
- package/package.json +5 -5
- package/postinstall.cjs +65 -0
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
│
|
|
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**:
|
|
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 (
|
|
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,
|
|
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/ #
|
|
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
|
-
**
|
|
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()`
|
|
68
|
-
The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token
|
|
69
|
-
in API URLs and replaces it with `bot
|
|
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
|
|
83
|
-
|
|
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_
|
|
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
|
-
`
|
|
122
|
+
`validate_socket_path()` rejects socket paths that:
|
|
122
123
|
- Contain `..` (directory traversal)
|
|
123
124
|
- Are not absolute (do not start with `/`)
|
|
124
|
-
- Exceed
|
|
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/
|
|
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_
|
|
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,
|
|
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.
|
|
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.
|
|
53
|
-
"@agidreams/ctm-linux-arm64": "0.2.
|
|
54
|
-
"@agidreams/ctm-darwin-arm64": "0.2.
|
|
55
|
-
"@agidreams/ctm-darwin-x64": "0.2.
|
|
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) {
|