feat(broker+cli): multi-tenant telegram bridge with 4 entry points
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

- DB: mesh.telegram_bridge table + migration
- Broker: telegram-bridge.ts (Grammy bot + WS pool + routing)
- Broker: telegram-token.ts (JWT connect tokens)
- Broker: POST /tg/token endpoint + bridge boot on startup
- CLI: claudemesh connect/disconnect telegram commands
- Spec: docs/telegram-bridge-spec.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-09 10:03:11 +01:00
parent c914f2b7db
commit 126bbfeb2c
11 changed files with 2120 additions and 3 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
/**
* JWT utilities for Telegram bridge connections.
*
* When a user connects their Telegram chat to a mesh, the broker generates
* a short-lived JWT containing mesh credentials. The Telegram bot decodes
* this token to establish the connection.
*
* Pure-crypto implementation — no external JWT library.
* Tokens are URL-safe (base64url) for use as Telegram deep link parameters.
*
* IMPORTANT: The JWT payload contains the member's secretKey.
* Never log the token or its decoded payload.
*/
import { createHmac } from "node:crypto";
// --- Types ---
export interface TelegramConnectPayload {
meshId: string;
meshSlug: string;
memberId: string;
pubkey: string;
secretKey: string; // ed25519 secret key — sensitive
createdBy: string; // Dashboard userId or CLI memberId
}
interface JwtClaims extends TelegramConnectPayload {
iss: string;
sub: string;
iat: number;
exp: number;
}
// --- Helpers ---
function base64url(data: string): string {
return Buffer.from(data).toString("base64url");
}
function base64urlDecode(str: string): string {
return Buffer.from(str, "base64url").toString("utf-8");
}
function sign(input: string, secret: string): string {
return createHmac("sha256", secret).update(input).digest("base64url");
}
// --- Public API ---
const JWT_HEADER = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const TOKEN_TTL_SECONDS = 15 * 60; // 15 minutes
/**
* Create a signed JWT containing Telegram connect credentials.
* Expires in 15 minutes.
*/
export function generateTelegramConnectToken(
payload: TelegramConnectPayload,
secret: string,
): string {
const now = Math.floor(Date.now() / 1000);
const claims: JwtClaims = {
...payload,
iss: "claudemesh-broker",
sub: "telegram-connect",
iat: now,
exp: now + TOKEN_TTL_SECONDS,
};
const encodedPayload = base64url(JSON.stringify(claims));
const signingInput = `${JWT_HEADER}.${encodedPayload}`;
const signature = sign(signingInput, secret);
return `${signingInput}.${signature}`;
}
/**
* Validate and decode a Telegram connect JWT.
* Returns the payload on success, or null on any failure
* (bad signature, expired, wrong subject).
*/
export function validateTelegramConnectToken(
token: string,
secret: string,
): TelegramConnectPayload | null {
try {
const parts = token.split(".");
if (parts.length !== 3) return null;
const [headerB64, payloadB64, signatureB64] = parts as [string, string, string];
// Verify signature
const signingInput = `${headerB64}.${payloadB64}`;
const expectedSignature = sign(signingInput, secret);
if (signatureB64 !== expectedSignature) return null;
// Verify header algorithm
const header = JSON.parse(base64urlDecode(headerB64));
if (header.alg !== "HS256") return null;
// Decode and validate claims
const claims: JwtClaims = JSON.parse(base64urlDecode(payloadB64));
// Check subject
if (claims.sub !== "telegram-connect") return null;
// Check expiry
const now = Math.floor(Date.now() / 1000);
if (claims.exp < now) return null;
// Check iat not in the future (30s tolerance)
if (claims.iat > now + 30) return null;
// Extract payload fields (strip JWT claims)
const {
meshId,
meshSlug,
memberId,
pubkey,
secretKey,
createdBy,
} = claims;
// Basic presence check
if (!meshId || !meshSlug || !memberId || !pubkey || !secretKey || !createdBy) {
return null;
}
return { meshId, meshSlug, memberId, pubkey, secretKey, createdBy };
} catch {
return null;
}
}
/**
* Generate a Telegram deep link that passes the JWT as start parameter.
* Format: https://t.me/{botUsername}?start={token}
*/
export function generateDeepLink(token: string, botUsername: string): string {
return `https://t.me/${botUsername}?start=${token}`;
}