Files
OmniRoute/src/lib/telegram/chatProxy.ts
Diego Rodrigues de Sa e Souza 0bb17b91c6 maint: final follow-up cherry-pick #9812 (#9907)
* fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main

Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici)
applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies,
and mermaid.

npm audit: 6→0 vulnerabilities.
Closes Dependabot #161-#166.

* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190)

Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13
(with monaco-editor scoped override). Closes Dependabot #189, #190.

Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge —
awaiting Dependabot re-scan.

npm audit → 0 vulnerabilities.

* fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks)

_tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing
slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential
_tasks symlink can slip in via git add -A and, once pulled, checkout materializes
it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks
ignores the symlink too, preventing re-capture.

* feat(telegram): Mini App chat bridge — initData auth, update webhook, chat proxy

Implements the Phase-1 slice of the Telegram Mini App integration
(docs/proposals/TELEGRAM-MINIAPP.md):

- src/lib/telegram/initData.ts — dependency-free WebApp initData HMAC-SHA256
  verification (Telegram Bot API spec), with auth_date freshness check.
- src/lib/telegram/config.ts — TELEGRAM_BOT_TOKEN / model / API base / timeout
  env config; token format validation; enabled gate.
- src/lib/telegram/botApi.ts — minimal fetch-based Bot API client
  (sendMessage, editMessageText, setWebhook) + update shape helpers.
- src/lib/telegram/chatProxy.ts — maps a Telegram user to a per-user
  OmniRoute API key (createApiKey, name telegram:<userId>) and proxies
  prompts through the existing handleChat pipeline.
- src/app/api/telegram/update/route.ts — inbound endpoint serving both the
  Bot API update webhook (/start + chat replies) and the Mini App direct
  path (initData HMAC verified → 401 on mismatch). Public route prefix;
  own auth only.
- src/app/miniapp/page.tsx — Telegram WebApp SDK chat UI.
- Tests: telegram-init-data (7), telegram-botapi (5) — 12/12 pass.
- Env docs: TELEGRAM_* vars in .env.example + ENVIRONMENT.md (sync ✓).
- Route-validation check: PASS (body validated via Zod).

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: benzntech <bensonkbmca@gmail.com>
2026-08-09 09:50:58 -03:00

107 lines
3.6 KiB
TypeScript

/**
* Telegram → OmniRoute chat proxy.
*
* Turns a plain Telegram message into a chat.completions call through the
* existing handleChat pipeline and returns the assistant text. Non-streaming
* for Phase 1 (Telegram has no native SSE); streaming is emulated later via
* progressive editMessageText.
*
* Auth model: each Telegram user is mapped to a generated OmniRoute API key
* (createApiKey) so the existing policy/rate-limit/model-allowlist machinery
* applies unchanged. The key is cached in-memory per user id.
*/
import { handleChat } from "@/sse/handlers/chat";
import { createApiKey, getApiKeys } from "@/lib/db/apiKeys";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { randomUUID } from "node:crypto";
const DEFAULT_MODEL = process.env.TELEGRAM_DEFAULT_MODEL || "auto/chat";
/**
* Resolve (and lazily mint) an OmniRoute API key for a Telegram user.
* Returns the plaintext key value, cached per user id.
*/
const keyCache = new Map<number, string>();
export async function resolveUserApiKey(telegramUserId: number): Promise<string> {
const cached = keyCache.get(telegramUserId);
if (cached) return cached;
const machineId = (await getConsistentMachineId().catch(() => null)) || "0000000000000000";
// Reuse an existing key whose name matches, else mint one.
const existing = await getApiKeys();
const match = existing?.find(
(k) =>
(k as { name?: string }).name === `telegram:${telegramUserId}` &&
typeof (k as { key?: string }).key === "string" &&
((k as { key?: string }).key?.length ?? 0) > 0
);
const matchKey = (match as { key?: string } | undefined)?.key;
if (typeof matchKey === "string" && matchKey.length > 0) {
keyCache.set(telegramUserId, matchKey);
return matchKey;
}
const created = await createApiKey(`telegram:${telegramUserId}`, machineId);
keyCache.set(telegramUserId, created.key);
return created.key;
}
function buildChatRequest(apiKey: string, prompt: string, model: string): Request {
const body = JSON.stringify({
model,
messages: [{ role: "user", content: prompt }],
stream: false,
});
const headers = new Headers({
"content-type": "application/json",
authorization: `Bearer ${apiKey}`,
});
return new Request("http://127.0.0.1/v1/chat/completions", {
method: "POST",
headers,
body,
});
}
/** Extract plain assistant text from a handleChat Response (stream or not). */
async function extractResponseText(response: Response): Promise<string> {
if (!response) return "";
if (response.body) {
// Non-streaming JSON: {"choices":[{"message":{"content": "..."}}]}
try {
const text = await response.text();
const json = JSON.parse(text) as {
choices?: Array<{ message?: { content?: string }; text?: string }>;
error?: { message?: string };
};
if (json.error?.message) return `⚠️ ${json.error.message}`;
const choice = json.choices?.[0];
return choice?.message?.content ?? choice?.text ?? "";
} catch {
return "";
}
}
return "";
}
/**
* Proxy one user prompt through the OmniRoute chat pipeline.
* @returns assistant text (may be empty on failure)
*/
export async function proxyChat(
telegramUserId: number,
prompt: string,
model = DEFAULT_MODEL
): Promise<string> {
if (!prompt?.trim()) return "";
const apiKey = await resolveUserApiKey(telegramUserId);
const request = buildChatRequest(apiKey, prompt.trim(), model);
const response = await handleChat(request, null, null);
return extractResponseText(response);
}
export { DEFAULT_MODEL };
export { randomUUID };