mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
* 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>
This commit is contained in:
committed by
GitHub
parent
3d590c310b
commit
0bb17b91c6
67
tests/unit/telegram-botapi.test.ts
Normal file
67
tests/unit/telegram-botapi.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHmac } from "node:crypto";
|
||||
|
||||
import { extractChatMessage } from "../../src/lib/telegram/botApi";
|
||||
import { verifyInitData } from "../../src/lib/telegram/initData";
|
||||
|
||||
const BOT_TOKEN = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
|
||||
|
||||
function buildValidInitData(botToken: string, fields: Record<string, string>): string {
|
||||
const pairs = Object.entries(fields).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
const dataCheckString = pairs.map(([k, v]) => `${k}=${v}`).join("\n");
|
||||
const secretKey = createHmac("sha256", "WebAppData").update(botToken).digest();
|
||||
const hash = createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
|
||||
const withHash = [...pairs, ["hash", hash]];
|
||||
return withHash.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
||||
}
|
||||
|
||||
test("extractChatMessage returns chatId/text/messageId for a text message", () => {
|
||||
const chat = extractChatMessage({
|
||||
update_id: 1,
|
||||
message: {
|
||||
message_id: 42,
|
||||
chat: { id: 123456789, type: "private" },
|
||||
text: "/start",
|
||||
from: { id: 123456789, first_name: "Benson" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(chat, { chatId: 123456789, text: "/start", messageId: 42 });
|
||||
});
|
||||
|
||||
test("extractChatMessage returns null for non-message updates", () => {
|
||||
const chat = extractChatMessage({ update_id: 2, callback_query: { id: "q", from: { id: 1 } } });
|
||||
assert.equal(chat, null);
|
||||
});
|
||||
|
||||
test("extractChatMessage returns null when text is missing", () => {
|
||||
const chat = extractChatMessage({
|
||||
update_id: 3,
|
||||
message: { message_id: 1, chat: { id: 1, type: "private" } },
|
||||
});
|
||||
assert.equal(chat, null);
|
||||
});
|
||||
|
||||
test("Mini App initData with real user payload verifies end-to-end", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
query_id: "AAHdF6IQAAAAAN0XohDhrOrc",
|
||||
user: '{"id":279058397,"first_name":"Benson","last_name":"KB","username":"benzntech"}',
|
||||
});
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN), true);
|
||||
// The same initData must fail with a different token (route would 401).
|
||||
assert.equal(verifyInitData(initData, "9876543210:ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvu"), false);
|
||||
});
|
||||
|
||||
test("Mini App initData fails when user field is swapped after signing", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
user: '{"id":279058397,"first_name":"Benson"}',
|
||||
});
|
||||
// Tamper with the user payload but keep the original hash.
|
||||
const parts = initData.split("&").filter((p) => !p.startsWith("hash="));
|
||||
const tampered = [...parts, "user=%7B%22id%22%3A1%2C%22first_name%22%3A%22Attacker%22%7D"].join(
|
||||
"&"
|
||||
);
|
||||
assert.equal(verifyInitData(tampered, BOT_TOKEN), false);
|
||||
});
|
||||
73
tests/unit/telegram-init-data.test.ts
Normal file
73
tests/unit/telegram-init-data.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHmac } from "node:crypto";
|
||||
|
||||
import { parseInitData, verifyInitData } from "../../src/lib/telegram/initData";
|
||||
|
||||
const BOT_TOKEN = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
|
||||
|
||||
/** Build a *valid* initData string for a given bot token (test helper). */
|
||||
function buildValidInitData(botToken: string, fields: Record<string, string>): string {
|
||||
const pairs = Object.entries(fields).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
const dataCheckString = pairs.map(([k, v]) => `${k}=${v}`).join("\n");
|
||||
const secretKey = createHmac("sha256", "WebAppData").update(botToken).digest();
|
||||
const hash = createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
|
||||
const withHash = [...pairs, ["hash", hash]];
|
||||
return withHash.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
||||
}
|
||||
|
||||
test("parseInitData decodes URL-encoded key/value pairs", () => {
|
||||
const parsed = parseInitData("user=%7B%22id%22%3A42%7D&auth_date=1700000000&hash=abc");
|
||||
assert.equal(parsed.user, '{"id":42}');
|
||||
assert.equal(parsed.auth_date, "1700000000");
|
||||
assert.equal(parsed.hash, "abc");
|
||||
});
|
||||
|
||||
test("verifyInitData accepts a valid signature", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
query_id: "AAHdF6IQAAAAAN0XohDhrOrc",
|
||||
user: '{"id":279058397,"first_name":"Benson","last_name":"KB","username":"benzntech"}',
|
||||
});
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN), true);
|
||||
});
|
||||
|
||||
test("verifyInitData rejects a tampered user field", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
user: '{"id":279058397,"first_name":"Benson"}',
|
||||
});
|
||||
const tampered = initData.replace("Benson", "Attacker");
|
||||
assert.equal(verifyInitData(tampered, BOT_TOKEN), false);
|
||||
});
|
||||
|
||||
test("verifyInitData rejects a wrong bot token", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
user: '{"id":1}',
|
||||
});
|
||||
assert.equal(verifyInitData(initData, "999:WRONGTOKENWRONGTOKENWRONGTOKENWRONG"), false);
|
||||
});
|
||||
|
||||
test("verifyInitData rejects missing hash", () => {
|
||||
const initData = "auth_date=1700000000&user=%7B%22id%22%3A1%7D";
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN), false);
|
||||
});
|
||||
|
||||
test("verifyInitData rejects stale auth_date beyond maxAge", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000) - 48 * 60 * 60), // 48h old
|
||||
user: '{"id":1}',
|
||||
});
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN, 24 * 60 * 60), false);
|
||||
// But passes when the window is generous
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN, 7 * 24 * 60 * 60), true);
|
||||
});
|
||||
|
||||
test("verifyInitData handles chunked/encoded keys", () => {
|
||||
const initData = buildValidInitData(BOT_TOKEN, {
|
||||
auth_date: String(Math.floor(Date.now() / 1000)),
|
||||
"some-key with spaces": "value with & specials",
|
||||
});
|
||||
assert.equal(verifyInitData(initData, BOT_TOKEN), true);
|
||||
});
|
||||
Reference in New Issue
Block a user