@justfortytwo/telegram 0.1.0
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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/adapter.d.ts +47 -0
- package/dist/adapter.js +79 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters-config.d.ts +11 -0
- package/dist/adapters-config.js +63 -0
- package/dist/adapters-config.js.map +1 -0
- package/dist/attachments.d.ts +26 -0
- package/dist/attachments.js +60 -0
- package/dist/attachments.js.map +1 -0
- package/dist/bindings.d.ts +36 -0
- package/dist/bindings.js +87 -0
- package/dist/bindings.js.map +1 -0
- package/dist/bridge.d.ts +63 -0
- package/dist/bridge.js +577 -0
- package/dist/bridge.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/login.d.ts +40 -0
- package/dist/login.js +88 -0
- package/dist/login.js.map +1 -0
- package/dist/telegram.d.ts +95 -0
- package/dist/telegram.js +152 -0
- package/dist/telegram.js.map +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enrico Deleo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @justfortytwo/telegram
|
|
2
|
+
|
|
3
|
+
The **Telegram channel adapter** for [fortytwo](https://github.com/justfortytwo) — a
|
|
4
|
+
standalone, always-on **bridge** that connects a Telegram bot to a headless
|
|
5
|
+
[`claude`](https://docs.claude.com/en/docs/claude-code) turn loop.
|
|
6
|
+
|
|
7
|
+
It does three jobs:
|
|
8
|
+
|
|
9
|
+
1. **Turn loop.** Long-poll Telegram, and for each authorized inbound message
|
|
10
|
+
drive a headless `claude -p --output-format json` turn (resuming the per-chat
|
|
11
|
+
session), then transport the assistant's reply back.
|
|
12
|
+
2. **Approval UX.** When the safety gate defers an external/irreversible tool
|
|
13
|
+
call, the turn ends with `stop_reason: tool_deferred`. The bridge surfaces an
|
|
14
|
+
**inline-keyboard approval card** (Approve / Deny, plus "Allow _N_h" for exact
|
|
15
|
+
Bash commands). The tap records the decision and resumes the turn so the
|
|
16
|
+
one-shot allow fires.
|
|
17
|
+
3. **Proactive wakes.** A scheduler invokes `claude -p` at fixed cron-like slots
|
|
18
|
+
(daily briefing, open-thread sweeps, weekly learn-review).
|
|
19
|
+
|
|
20
|
+
## Pairing / login binding model
|
|
21
|
+
|
|
22
|
+
The bot is **locked down**: an unbound sender that does not present a valid
|
|
23
|
+
`/login` gets **no response at all**. Access is earned through a
|
|
24
|
+
channel-agnostic challenge/verify pairing flow:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
owner side: adapter.issueChallenge(owner) -> { code, ttl }
|
|
28
|
+
channel side: /login <code> -> verify(chatId, code) -> binding
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `issueChallenge(owner)` mints a **single-use, short-TTL** code (default 6
|
|
32
|
+
digits, 5 minutes). The owner relays it out-of-band to the chat they want to
|
|
33
|
+
bind.
|
|
34
|
+
- `/login <code>` verifies the code and, on success, **persists a binding**
|
|
35
|
+
`(channelType, channelUserId) -> owner`.
|
|
36
|
+
- `/logout` removes the binding.
|
|
37
|
+
- Every inbound message is authorized against **persisted bindings ∪ the
|
|
38
|
+
optional `ALLOWED_CHAT_IDS` bootstrap set**. The bootstrap set lets the first
|
|
39
|
+
owner chat reach the bridge before any binding exists; it is not required once
|
|
40
|
+
bindings exist.
|
|
41
|
+
|
|
42
|
+
The contract is intentionally **channel-agnostic** (`ChannelAdapter` in
|
|
43
|
+
`src/adapter.ts`) so a future Slack / email / SMS adapter implements the same
|
|
44
|
+
`issueChallenge` / `verify` shape.
|
|
45
|
+
|
|
46
|
+
### Binding store
|
|
47
|
+
|
|
48
|
+
Authorization must work **even when the memory package is absent**, so the
|
|
49
|
+
binding store is **self-owned** and does not assume the `@justfortytwo/memory`
|
|
50
|
+
sqlite db exists:
|
|
51
|
+
|
|
52
|
+
- `SqliteBindingStore` — default; a tiny `better-sqlite3` table in its own db
|
|
53
|
+
file (`state/telegram-bindings.db` by default).
|
|
54
|
+
- `MemoryBindingStore` — in-process Map, for tests / ephemeral use.
|
|
55
|
+
- `BindingStore` — the injectable interface. Supply a memory-backed
|
|
56
|
+
implementation later without touching the login flow.
|
|
57
|
+
|
|
58
|
+
> Cross-repo reconciliation: if fortytwo standardizes a shared identity
|
|
59
|
+
> table, swap the default store for a memory-backed `BindingStore`. The login
|
|
60
|
+
> flow is unaffected.
|
|
61
|
+
|
|
62
|
+
## Peer dependencies
|
|
63
|
+
|
|
64
|
+
This package orchestrates two **peer** packages — they are referenced, not
|
|
65
|
+
bundled:
|
|
66
|
+
|
|
67
|
+
| Peer | Used for | Contract |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `@justfortytwo/memory` | Memory store, jobs, pending-decision records, embeddings | `MEMORY_TOOL_CONTRACT_VERSION` (tools `mcp__fortytwo-memory__*`) |
|
|
70
|
+
| `@justfortytwo/gate` | Bash exact-command allowlist behind "Allow _N_h"; provenance envelopes | `POLICY_SCHEMA_VERSION` |
|
|
71
|
+
|
|
72
|
+
Install them alongside this package:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npm install @justfortytwo/telegram @justfortytwo/memory @justfortytwo/gate
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The seams where these peers are wired are marked with `// TODO(wire):` in
|
|
79
|
+
`src/bridge.ts`, `src/attachments.ts`, and `src/adapters-config.ts`. Until they
|
|
80
|
+
are wired, the bridge runs **channel-only** (no memory/jobs) via no-op shims.
|
|
81
|
+
|
|
82
|
+
## Environment
|
|
83
|
+
|
|
84
|
+
| Var | Required | Meaning |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `TELEGRAM_BOT_TOKEN` | yes | Bot token. The bridge is the **only** long-poller on this bot. |
|
|
87
|
+
| `ALLOWED_CHAT_IDS` | bootstrap | Comma-separated chat IDs allowed before any binding exists. Optional once bindings exist. |
|
|
88
|
+
| `CLAUDE_BIN` | no | Path to the `claude` binary (default `claude`). |
|
|
89
|
+
| `FORD_TURN_TIMEOUT` | no | Hard cap (seconds) on a single turn so a stalled model can't wedge the bridge (default 300). |
|
|
90
|
+
| `TELEGRAM_BINDINGS_DB` | no | Path to the self-owned bindings db (default `state/telegram-bindings.db`). |
|
|
91
|
+
| `ASSISTANT_NAME` / `ASSISTANT_ACTOR` / `OWNER_ACTOR` | no | Display name + journal actor labels. |
|
|
92
|
+
| `FORD_ROOT` | no | Working root for the headless `claude` process + state files. |
|
|
93
|
+
|
|
94
|
+
> Proxy auth for the headless `claude` session is **not** read here — the
|
|
95
|
+
> `claude` CLI reads `ANTHROPIC_*` from its own config.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npm run build
|
|
101
|
+
node dist/bridge.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run it under a restart loop (tmux/systemd) so it survives transient errors and
|
|
105
|
+
crashes. Example launcher (adapted from the monolith's `wakeup.sh`):
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
set -a; source .env; set +a
|
|
109
|
+
while true; do
|
|
110
|
+
node node_modules/@justfortytwo/telegram/dist/bridge.js
|
|
111
|
+
echo "[telegram] bridge exited (rc=$?) — restarting in 3s"; sleep 3
|
|
112
|
+
done
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT © 2026 Enrico Deleo
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
Created and maintained by [**Enrico Deleo**](https://enricodeleo.com).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Binding, BindingStore, ChannelType } from './bindings.js';
|
|
2
|
+
export interface Challenge {
|
|
3
|
+
/** The single-use pairing code the owner relays to the channel. */
|
|
4
|
+
code: string;
|
|
5
|
+
/** Seconds until the code expires. */
|
|
6
|
+
ttl: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ChannelAdapter {
|
|
9
|
+
/** The channel this adapter binds (e.g. 'telegram'). */
|
|
10
|
+
readonly channelType: ChannelType;
|
|
11
|
+
/** Mint a single-use, short-TTL pairing code for `owner`. */
|
|
12
|
+
issueChallenge(owner: string): Challenge;
|
|
13
|
+
/**
|
|
14
|
+
* Verify `proof` (a presented code) for `channelUserId`. On success, persist
|
|
15
|
+
* and return the Binding; on failure (bad/expired/used code) return null.
|
|
16
|
+
*/
|
|
17
|
+
verify(channelUserId: string, proof: string): Binding | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ChallengeOptions {
|
|
20
|
+
/** Code lifetime in seconds. Default 300 (5 min). Short by design. */
|
|
21
|
+
ttlSeconds?: number;
|
|
22
|
+
/** Number of digits in the generated code. Default 6. */
|
|
23
|
+
digits?: number;
|
|
24
|
+
/** Injectable clock for tests. */
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Telegram implementation of ChannelAdapter. Codes live in-process (a wedged
|
|
29
|
+
* bridge restart drops outstanding codes — acceptable for a short-TTL,
|
|
30
|
+
* owner-initiated flow). Verified bindings persist via the injected BindingStore.
|
|
31
|
+
*/
|
|
32
|
+
export declare class TelegramAdapter implements ChannelAdapter {
|
|
33
|
+
private readonly store;
|
|
34
|
+
readonly channelType: ChannelType;
|
|
35
|
+
private readonly pending;
|
|
36
|
+
private readonly ttlSeconds;
|
|
37
|
+
private readonly digits;
|
|
38
|
+
private readonly now;
|
|
39
|
+
constructor(store: BindingStore, opts?: ChallengeOptions);
|
|
40
|
+
issueChallenge(owner: string): Challenge;
|
|
41
|
+
verify(channelUserId: string, proof: string): Binding | null;
|
|
42
|
+
/** Look up the persisted binding for a channel id (no proof needed). */
|
|
43
|
+
bindingFor(channelUserId: string): Binding | null;
|
|
44
|
+
/** Remove a persisted binding (logout). */
|
|
45
|
+
unbind(channelUserId: string): void;
|
|
46
|
+
private sweepExpired;
|
|
47
|
+
}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Channel-agnostic binding contract.
|
|
2
|
+
//
|
|
3
|
+
// A ChannelAdapter knows how to pair a channel identity (e.g. a Telegram chat
|
|
4
|
+
// id) with a logical `owner`. Pairing is a two-step challenge/verify:
|
|
5
|
+
//
|
|
6
|
+
// 1. issueChallenge(owner) -> { code, ttl }
|
|
7
|
+
// Owner-side: mint a single-use, short-TTL code. The owner relays this code
|
|
8
|
+
// out-of-band to the channel they want to bind (e.g. "DM the bot
|
|
9
|
+
// `/login 482913`").
|
|
10
|
+
// 2. verify(channelUserId, proof) -> binding | null
|
|
11
|
+
// Channel-side: when an inbound message presents the code, confirm it is
|
|
12
|
+
// valid + unexpired + unused, then mint and persist a Binding.
|
|
13
|
+
//
|
|
14
|
+
// This keeps authorization independent of any single channel: a future Slack /
|
|
15
|
+
// email / SMS adapter implements the same contract.
|
|
16
|
+
import { randomInt } from 'node:crypto';
|
|
17
|
+
/**
|
|
18
|
+
* Telegram implementation of ChannelAdapter. Codes live in-process (a wedged
|
|
19
|
+
* bridge restart drops outstanding codes — acceptable for a short-TTL,
|
|
20
|
+
* owner-initiated flow). Verified bindings persist via the injected BindingStore.
|
|
21
|
+
*/
|
|
22
|
+
export class TelegramAdapter {
|
|
23
|
+
store;
|
|
24
|
+
channelType = 'telegram';
|
|
25
|
+
pending = new Map(); // code -> challenge
|
|
26
|
+
ttlSeconds;
|
|
27
|
+
digits;
|
|
28
|
+
now;
|
|
29
|
+
constructor(store, opts = {}) {
|
|
30
|
+
this.store = store;
|
|
31
|
+
this.ttlSeconds = opts.ttlSeconds ?? 300;
|
|
32
|
+
this.digits = opts.digits ?? 6;
|
|
33
|
+
this.now = opts.now ?? (() => Date.now());
|
|
34
|
+
}
|
|
35
|
+
issueChallenge(owner) {
|
|
36
|
+
this.sweepExpired();
|
|
37
|
+
const max = 10 ** this.digits;
|
|
38
|
+
const code = String(randomInt(0, max)).padStart(this.digits, '0');
|
|
39
|
+
this.pending.set(code, { code, owner, expiresAt: this.now() + this.ttlSeconds * 1000 });
|
|
40
|
+
return { code, ttl: this.ttlSeconds };
|
|
41
|
+
}
|
|
42
|
+
verify(channelUserId, proof) {
|
|
43
|
+
this.sweepExpired();
|
|
44
|
+
const code = proof.trim();
|
|
45
|
+
const challenge = this.pending.get(code);
|
|
46
|
+
if (!challenge)
|
|
47
|
+
return null;
|
|
48
|
+
if (challenge.expiresAt <= this.now()) {
|
|
49
|
+
this.pending.delete(code);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Single-use: consume the code regardless of downstream outcome.
|
|
53
|
+
this.pending.delete(code);
|
|
54
|
+
const binding = {
|
|
55
|
+
channelType: this.channelType,
|
|
56
|
+
channelUserId,
|
|
57
|
+
owner: challenge.owner,
|
|
58
|
+
boundAt: new Date(this.now()).toISOString(),
|
|
59
|
+
};
|
|
60
|
+
this.store.put(binding);
|
|
61
|
+
return binding;
|
|
62
|
+
}
|
|
63
|
+
/** Look up the persisted binding for a channel id (no proof needed). */
|
|
64
|
+
bindingFor(channelUserId) {
|
|
65
|
+
return this.store.get(this.channelType, channelUserId);
|
|
66
|
+
}
|
|
67
|
+
/** Remove a persisted binding (logout). */
|
|
68
|
+
unbind(channelUserId) {
|
|
69
|
+
this.store.remove(this.channelType, channelUserId);
|
|
70
|
+
}
|
|
71
|
+
sweepExpired() {
|
|
72
|
+
const t = this.now();
|
|
73
|
+
for (const [code, c] of this.pending) {
|
|
74
|
+
if (c.expiresAt <= t)
|
|
75
|
+
this.pending.delete(code);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface AdaptersConfig {
|
|
2
|
+
channelTelegramEnabled: boolean;
|
|
3
|
+
attachments: {
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
storeDir: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Type of the gate's `parseToml`: every scalar comes back as a string. */
|
|
9
|
+
export type TomlParser = (text: string) => Record<string, unknown>;
|
|
10
|
+
/** Load the drop-in adapter manifest. Secrets stay in env; this is selection + non-secret settings only. */
|
|
11
|
+
export declare function loadAdaptersConfig(path: string, parseToml?: TomlParser): AdaptersConfig;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
channelTelegramEnabled: true,
|
|
4
|
+
attachments: { maxBytes: 26214400, storeDir: 'inbox' }, // 25 MiB
|
|
5
|
+
};
|
|
6
|
+
function asRecord(v) {
|
|
7
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Minimal flat-table TOML reader (fallback only). Understands `[section]`
|
|
11
|
+
* headers and `key = value` pairs, emitting dotted keys ("section.key") with
|
|
12
|
+
* string values — matching the surface the original `parseToml` exposed for
|
|
13
|
+
* adapters.toml. Not a general TOML implementation; replace with the gate parser.
|
|
14
|
+
*/
|
|
15
|
+
function fallbackParseToml(text) {
|
|
16
|
+
const out = {};
|
|
17
|
+
let section = '';
|
|
18
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
19
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
20
|
+
if (!line)
|
|
21
|
+
continue;
|
|
22
|
+
const header = line.match(/^\[(.+)\]$/);
|
|
23
|
+
if (header) {
|
|
24
|
+
section = header[1].trim();
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const kv = line.match(/^([^=]+)=(.*)$/);
|
|
28
|
+
if (!kv)
|
|
29
|
+
continue;
|
|
30
|
+
const key = kv[1].trim();
|
|
31
|
+
let value = kv[2].trim();
|
|
32
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
const table = (out[section] ??= {});
|
|
36
|
+
table[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Load the drop-in adapter manifest. Secrets stay in env; this is selection + non-secret settings only. */
|
|
41
|
+
export function loadAdaptersConfig(path, parseToml = fallbackParseToml) {
|
|
42
|
+
if (!existsSync(path))
|
|
43
|
+
return DEFAULTS;
|
|
44
|
+
let o;
|
|
45
|
+
try {
|
|
46
|
+
o = parseToml(readFileSync(path, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return DEFAULTS;
|
|
50
|
+
}
|
|
51
|
+
// parseToml returns every scalar as a string — coerce here.
|
|
52
|
+
const channel = asRecord(o['channel.telegram'] ?? asRecord(o['channel'])['telegram']);
|
|
53
|
+
const att = asRecord(o['attachments']);
|
|
54
|
+
const maxBytes = Number(att['max_bytes']);
|
|
55
|
+
return {
|
|
56
|
+
channelTelegramEnabled: channel['enabled'] === undefined ? true : channel['enabled'] === 'true',
|
|
57
|
+
attachments: {
|
|
58
|
+
maxBytes: Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULTS.attachments.maxBytes,
|
|
59
|
+
storeDir: typeof att['store_dir'] === 'string' && att['store_dir'] ? att['store_dir'] : DEFAULTS.attachments.storeDir,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=adapters-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters-config.js","sourceRoot":"","sources":["../src/adapters-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type SourceEnvelope, type SourceKind } from '@justfortytwo/gate';
|
|
2
|
+
export type AttachmentKind = 'image' | 'document' | 'audio' | 'other';
|
|
3
|
+
export interface Attachment {
|
|
4
|
+
attachment_id: string;
|
|
5
|
+
kind: AttachmentKind;
|
|
6
|
+
mime: string;
|
|
7
|
+
storage_ref: string;
|
|
8
|
+
byte_size: number;
|
|
9
|
+
content_hash: string;
|
|
10
|
+
caption?: string;
|
|
11
|
+
source_envelope: SourceEnvelope;
|
|
12
|
+
}
|
|
13
|
+
export declare function kindFromMime(mime: string): AttachmentKind;
|
|
14
|
+
export declare function extensionFor(mime: string, fileName?: string): string;
|
|
15
|
+
export declare function inboxRelPath(storeDir: string, chatId: number, messageId: number, index: number, ext: string): string;
|
|
16
|
+
export declare function withinSizeLimit(byteSize: number, maxBytes: number): boolean;
|
|
17
|
+
export declare function buildAttachment(args: {
|
|
18
|
+
bytes: Uint8Array;
|
|
19
|
+
mime: string;
|
|
20
|
+
sourceKind: SourceKind | string;
|
|
21
|
+
storageRef: string;
|
|
22
|
+
channel: string;
|
|
23
|
+
actor?: string;
|
|
24
|
+
caption?: string;
|
|
25
|
+
fileName?: string;
|
|
26
|
+
}): Attachment;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// TODO(wire): SourceKind / SourceEnvelope / createSourceEnvelope are the
|
|
3
|
+
// provenance-envelope primitives owned by the safety gate (@justfortytwo/gate),
|
|
4
|
+
// a peer package — NOT this one. gate's SourceKind is a host-agnostic union that
|
|
5
|
+
// allows a channel to map its own kinds in as plain strings, which is how the
|
|
6
|
+
// telegram_* kinds below flow through.
|
|
7
|
+
import { createSourceEnvelope } from '@justfortytwo/gate';
|
|
8
|
+
const EXT_BY_MIME = {
|
|
9
|
+
'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp',
|
|
10
|
+
'application/pdf': 'pdf', 'text/plain': 'txt',
|
|
11
|
+
};
|
|
12
|
+
export function kindFromMime(mime) {
|
|
13
|
+
if (mime.startsWith('image/'))
|
|
14
|
+
return 'image';
|
|
15
|
+
if (mime.startsWith('audio/'))
|
|
16
|
+
return 'audio';
|
|
17
|
+
if (mime === 'application/pdf' || mime.startsWith('text/') || mime.startsWith('application/'))
|
|
18
|
+
return 'document';
|
|
19
|
+
return 'other';
|
|
20
|
+
}
|
|
21
|
+
export function extensionFor(mime, fileName) {
|
|
22
|
+
if (EXT_BY_MIME[mime])
|
|
23
|
+
return EXT_BY_MIME[mime];
|
|
24
|
+
const dot = fileName ? fileName.lastIndexOf('.') : -1;
|
|
25
|
+
if (fileName && dot >= 0 && dot < fileName.length - 1) {
|
|
26
|
+
return fileName.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, '') || 'bin';
|
|
27
|
+
}
|
|
28
|
+
return 'bin';
|
|
29
|
+
}
|
|
30
|
+
export function inboxRelPath(storeDir, chatId, messageId, index, ext) {
|
|
31
|
+
const suffix = index > 0 ? `-${index}` : '';
|
|
32
|
+
return `${storeDir}/${chatId}/${messageId}${suffix}.${ext}`;
|
|
33
|
+
}
|
|
34
|
+
export function withinSizeLimit(byteSize, maxBytes) {
|
|
35
|
+
return byteSize > 0 && byteSize <= maxBytes;
|
|
36
|
+
}
|
|
37
|
+
function sha256Bytes(bytes) {
|
|
38
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
export function buildAttachment(args) {
|
|
41
|
+
const contentHash = sha256Bytes(args.bytes);
|
|
42
|
+
const envelope = createSourceEnvelope({
|
|
43
|
+
source_kind: args.sourceKind,
|
|
44
|
+
content: contentHash,
|
|
45
|
+
actor: args.actor,
|
|
46
|
+
channel: args.channel,
|
|
47
|
+
metadata: { mime: args.mime, byte_size: args.bytes.length, storage_ref: args.storageRef, file_name: args.fileName },
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
attachment_id: envelope.source_id,
|
|
51
|
+
kind: kindFromMime(args.mime),
|
|
52
|
+
mime: args.mime,
|
|
53
|
+
storage_ref: args.storageRef,
|
|
54
|
+
byte_size: args.bytes.length,
|
|
55
|
+
content_hash: contentHash,
|
|
56
|
+
caption: args.caption,
|
|
57
|
+
source_envelope: envelope,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=attachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ChannelType = 'telegram' | string;
|
|
2
|
+
export interface Binding {
|
|
3
|
+
channelType: ChannelType;
|
|
4
|
+
channelUserId: string;
|
|
5
|
+
owner: string;
|
|
6
|
+
boundAt: string;
|
|
7
|
+
}
|
|
8
|
+
/** The seam. Persists channel-id -> owner bindings; everything else injects this. */
|
|
9
|
+
export interface BindingStore {
|
|
10
|
+
get(channelType: ChannelType, channelUserId: string): Binding | null;
|
|
11
|
+
put(binding: Binding): void;
|
|
12
|
+
remove(channelType: ChannelType, channelUserId: string): void;
|
|
13
|
+
list(channelType?: ChannelType): Binding[];
|
|
14
|
+
}
|
|
15
|
+
/** In-memory store — ideal for tests and ephemeral deployments. */
|
|
16
|
+
export declare class MemoryBindingStore implements BindingStore {
|
|
17
|
+
private readonly map;
|
|
18
|
+
private key;
|
|
19
|
+
get(channelType: ChannelType, channelUserId: string): Binding | null;
|
|
20
|
+
put(binding: Binding): void;
|
|
21
|
+
remove(channelType: ChannelType, channelUserId: string): void;
|
|
22
|
+
list(channelType?: ChannelType): Binding[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Self-owned sqlite-backed binding store. Uses its own small db file so it does
|
|
26
|
+
* NOT depend on the memory package's schema or db existence. Lazily creates the
|
|
27
|
+
* table on construction.
|
|
28
|
+
*/
|
|
29
|
+
export declare class SqliteBindingStore implements BindingStore {
|
|
30
|
+
private readonly db;
|
|
31
|
+
constructor(dbPath?: string);
|
|
32
|
+
get(channelType: ChannelType, channelUserId: string): Binding | null;
|
|
33
|
+
put(binding: Binding): void;
|
|
34
|
+
remove(channelType: ChannelType, channelUserId: string): void;
|
|
35
|
+
list(channelType?: ChannelType): Binding[];
|
|
36
|
+
}
|
package/dist/bindings.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Binding store: persists (channelType, channelUserId) -> owner.
|
|
2
|
+
//
|
|
3
|
+
// DESIGN DECISION (cross-repo reconciliation point): the telegram adapter must
|
|
4
|
+
// NOT assume the @justfortytwo/memory sqlite db exists. Authorization is the
|
|
5
|
+
// channel's own concern and must work even when memory is absent. So this file
|
|
6
|
+
// defines:
|
|
7
|
+
// 1. `BindingStore` — an injectable interface (the seam). Anyone can supply a
|
|
8
|
+
// memory-backed implementation later without changing the login flow.
|
|
9
|
+
// 2. `SqliteBindingStore` — a small SELF-OWNED better-sqlite3 store (its own
|
|
10
|
+
// tiny db file, default ./telegram-bindings.db) used by default.
|
|
11
|
+
// 3. `MemoryBindingStore` — an in-process Map store for tests / ephemeral use.
|
|
12
|
+
//
|
|
13
|
+
// If/when fortytwo standardizes a shared identity table, swap the default
|
|
14
|
+
// for a memory-backed BindingStore — the login flow is unaffected.
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
/** In-memory store — ideal for tests and ephemeral deployments. */
|
|
17
|
+
export class MemoryBindingStore {
|
|
18
|
+
map = new Map();
|
|
19
|
+
key(t, u) { return `${t} ${u}`; }
|
|
20
|
+
get(channelType, channelUserId) {
|
|
21
|
+
return this.map.get(this.key(channelType, channelUserId)) ?? null;
|
|
22
|
+
}
|
|
23
|
+
put(binding) {
|
|
24
|
+
this.map.set(this.key(binding.channelType, binding.channelUserId), binding);
|
|
25
|
+
}
|
|
26
|
+
remove(channelType, channelUserId) {
|
|
27
|
+
this.map.delete(this.key(channelType, channelUserId));
|
|
28
|
+
}
|
|
29
|
+
list(channelType) {
|
|
30
|
+
const all = [...this.map.values()];
|
|
31
|
+
return channelType ? all.filter((b) => b.channelType === channelType) : all;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Load the native sqlite driver via createRequire so this ESM module can pull a
|
|
35
|
+
// CommonJS addon without a top-level static import (which would force the native
|
|
36
|
+
// dependency on type-only consumers).
|
|
37
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
38
|
+
/**
|
|
39
|
+
* Self-owned sqlite-backed binding store. Uses its own small db file so it does
|
|
40
|
+
* NOT depend on the memory package's schema or db existence. Lazily creates the
|
|
41
|
+
* table on construction.
|
|
42
|
+
*/
|
|
43
|
+
export class SqliteBindingStore {
|
|
44
|
+
db;
|
|
45
|
+
constructor(dbPath = 'telegram-bindings.db') {
|
|
46
|
+
const Database = nodeRequire('better-sqlite3');
|
|
47
|
+
this.db = new Database(dbPath);
|
|
48
|
+
this.db.pragma('journal_mode = WAL');
|
|
49
|
+
this.db.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS channel_bindings (
|
|
51
|
+
channel_type TEXT NOT NULL,
|
|
52
|
+
channel_user_id TEXT NOT NULL,
|
|
53
|
+
owner TEXT NOT NULL,
|
|
54
|
+
bound_at TEXT NOT NULL,
|
|
55
|
+
PRIMARY KEY (channel_type, channel_user_id)
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
get(channelType, channelUserId) {
|
|
60
|
+
const row = this.db
|
|
61
|
+
.prepare('SELECT channel_type, channel_user_id, owner, bound_at FROM channel_bindings WHERE channel_type = ? AND channel_user_id = ?')
|
|
62
|
+
.get(channelType, channelUserId);
|
|
63
|
+
return row ? { channelType: row.channel_type, channelUserId: row.channel_user_id, owner: row.owner, boundAt: row.bound_at } : null;
|
|
64
|
+
}
|
|
65
|
+
put(binding) {
|
|
66
|
+
this.db
|
|
67
|
+
.prepare(`
|
|
68
|
+
INSERT INTO channel_bindings (channel_type, channel_user_id, owner, bound_at)
|
|
69
|
+
VALUES (@channelType, @channelUserId, @owner, @boundAt)
|
|
70
|
+
ON CONFLICT(channel_type, channel_user_id)
|
|
71
|
+
DO UPDATE SET owner = excluded.owner, bound_at = excluded.bound_at
|
|
72
|
+
`)
|
|
73
|
+
.run(binding);
|
|
74
|
+
}
|
|
75
|
+
remove(channelType, channelUserId) {
|
|
76
|
+
this.db
|
|
77
|
+
.prepare('DELETE FROM channel_bindings WHERE channel_type = ? AND channel_user_id = ?')
|
|
78
|
+
.run(channelType, channelUserId);
|
|
79
|
+
}
|
|
80
|
+
list(channelType) {
|
|
81
|
+
const rows = (channelType
|
|
82
|
+
? this.db.prepare('SELECT channel_type, channel_user_id, owner, bound_at FROM channel_bindings WHERE channel_type = ?').all(channelType)
|
|
83
|
+
: this.db.prepare('SELECT channel_type, channel_user_id, owner, bound_at FROM channel_bindings').all());
|
|
84
|
+
return rows.map((r) => ({ channelType: r.channel_type, channelUserId: r.channel_user_id, owner: r.owner, boundAt: r.bound_at }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bindings.js","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type TgMessage, type AttachmentSpec } from './telegram.js';
|
|
3
|
+
import { type Attachment } from './attachments.js';
|
|
4
|
+
import type { SourceKind } from '@justfortytwo/gate';
|
|
5
|
+
import type { MemoryInput } from '@justfortytwo/memory';
|
|
6
|
+
interface ChannelEvent {
|
|
7
|
+
channel: string;
|
|
8
|
+
direction: string;
|
|
9
|
+
actor: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
content: string;
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
13
|
+
thread_id?: string | null;
|
|
14
|
+
approval_status?: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** Map a channel event onto memory's generic MemoryInput (content + provenance). */
|
|
17
|
+
export declare function mapChannelEventToMemoryInput(entry: ChannelEvent): MemoryInput;
|
|
18
|
+
export interface DeferredTool {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
input: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface Interpreted {
|
|
24
|
+
reply?: string;
|
|
25
|
+
deferred?: DeferredTool;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Read a `claude -p --output-format json` result into a reply OR a deferred tool. */
|
|
29
|
+
export declare function interpretResult(r: any): Interpreted;
|
|
30
|
+
export declare function formatApprovalCard(d: DeferredTool, bashTtlHours?: number): string;
|
|
31
|
+
export interface Schedule {
|
|
32
|
+
name: string;
|
|
33
|
+
minute: number;
|
|
34
|
+
hour: number;
|
|
35
|
+
dow?: number;
|
|
36
|
+
trigger: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function telegramSourceKind(message: TgMessage): SourceKind;
|
|
39
|
+
export declare function hasHandleableContent(message: TgMessage): boolean;
|
|
40
|
+
export declare function telegramCommandReply(text: string): string | undefined;
|
|
41
|
+
export declare function buildInboundPrompt(caption: string, attachments: Attachment[]): string;
|
|
42
|
+
export interface AttachmentIo {
|
|
43
|
+
download(fileId: string): Promise<Buffer>;
|
|
44
|
+
write(relPath: string, bytes: Buffer): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Download + store each attachment in a message, returning provenance-tagged
|
|
48
|
+
* Attachments. Pure given an injected io (no network/fs in tests). Two-stage
|
|
49
|
+
* size check: declared file_size for early reject, actual length for hard enforcement.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ingestMessageAttachments(io: AttachmentIo, message: TgMessage, opts: {
|
|
52
|
+
maxBytes: number;
|
|
53
|
+
storeDir: string;
|
|
54
|
+
channel: string;
|
|
55
|
+
actor?: string;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
attachments: Attachment[];
|
|
58
|
+
skipped: AttachmentSpec[];
|
|
59
|
+
}>;
|
|
60
|
+
export declare const SCHEDULES: Schedule[];
|
|
61
|
+
/** Schedules whose cron slot matches `now` and haven't fired in this slot yet. */
|
|
62
|
+
export declare function dueSchedules(now: Date, fired: Record<string, string>): Schedule[];
|
|
63
|
+
export {};
|