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

View File

@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS mesh.telegram_bridge (
id text PRIMARY KEY NOT NULL,
chat_id bigint NOT NULL,
chat_type text DEFAULT 'private',
chat_title text,
mesh_id text NOT NULL REFERENCES mesh.mesh(id) ON DELETE CASCADE ON UPDATE CASCADE,
member_id text REFERENCES mesh.member(id),
pubkey text NOT NULL,
secret_key text NOT NULL,
display_name text DEFAULT 'telegram',
active boolean DEFAULT true,
created_at timestamp DEFAULT now() NOT NULL,
disconnected_at timestamp
);
CREATE UNIQUE INDEX IF NOT EXISTS telegram_bridge_chat_mesh_idx ON mesh.telegram_bridge (chat_id, mesh_id);

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import {
bigint,
boolean,
index,
integer,
@@ -909,3 +910,51 @@ export const selectMeshVaultEntrySchema = createSelectSchema(meshVaultEntry);
export const insertMeshVaultEntrySchema = createInsertSchema(meshVaultEntry);
export type SelectMeshVaultEntry = typeof meshVaultEntry.$inferSelect;
export type InsertMeshVaultEntry = typeof meshVaultEntry.$inferInsert;
/**
* Telegram bridge connections. Each row represents a Telegram chat linked
* to a mesh via a bot-managed keypair. The bot authenticates to the broker
* as a virtual peer using the ed25519 keypair stored here, relaying
* messages bidirectionally between Telegram and the mesh.
*/
export const telegramBridge = meshSchema.table(
"telegram_bridge",
{
id: text().primaryKey().notNull().$defaultFn(generateId),
/** Telegram chat ID (can be negative for groups). */
chatId: bigint({ mode: "bigint" }).notNull(),
chatType: text().default("private"),
chatTitle: text(),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
memberId: text().references(() => meshMember.id),
/** ed25519 public key (hex) — the virtual peer identity on the mesh. */
pubkey: text().notNull(),
/** ed25519 secret key (hex) — encrypted at rest. */
secretKey: text().notNull(),
displayName: text().default("telegram"),
active: boolean().default(true),
createdAt: timestamp().defaultNow().notNull(),
disconnectedAt: timestamp(),
},
(table) => [
uniqueIndex("telegram_bridge_chat_mesh_idx").on(table.chatId, table.meshId),
],
);
export const telegramBridgeRelations = relations(telegramBridge, ({ one }) => ({
mesh: one(mesh, {
fields: [telegramBridge.meshId],
references: [mesh.id],
}),
member: one(meshMember, {
fields: [telegramBridge.memberId],
references: [meshMember.id],
}),
}));
export const selectTelegramBridgeSchema = createSelectSchema(telegramBridge);
export const insertTelegramBridgeSchema = createInsertSchema(telegramBridge);
export type SelectTelegramBridge = typeof telegramBridge.$inferSelect;
export type InsertTelegramBridge = typeof telegramBridge.$inferInsert;