Files
HealthLog/src/app/api/telegram/webhook/route.ts
T
Marc-André Bombeck 5433fc8fbd chore(release): v1.28.26 — internal restructuring for maintainability
No user-facing change. Eight of the largest source files split along their
natural seams into focused modules, every move verbatim with stable import
paths:

- mood analytics calculators (mood-tag-influence / mood-crosstab /
  mood-better-days / mood-patterns; hub 2229 → 770 lines)
- insight status-invalidation machinery (status-invalidation.ts)
- feature extraction (feature-blocks / signals-of-day)
- doctor report types + pure helpers (doctor-report-types /
  doctor-report-helpers; collector 1756 → 1002)
- Google Health mapping layer (mappers.ts; client 2153 → 1022, acyclic
  client → mappers with type-only back-imports)
- coach chat bubbles + read-aloud controls (chat-bubble / read-aloud;
  message-thread 1713 → 464)
- Telegram webhook handlers to lib (route 1307 → 92 — handler thin, logic in
  lib; rate-limit-before-secret ordering byte-identical)
- coach snapshot cache, series helpers and the four largest per-metric blocks
  (snapshot-cache / snapshot-series / snapshot-blocks; 2691 → 1896)

The glucose-panel-window structural test follows the moved alias; two unused
imports left by the feature split are removed.
2026-07-11 08:43:41 +02:00

93 lines
3.1 KiB
TypeScript

import { timingSafeEqual } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { getClientIp } from "@/lib/api-response";
import { annotate, getEvent } from "@/lib/logging/context";
import {
handleCallback,
handleTextMessage,
type TelegramUpdate,
} from "@/lib/telegram-webhook-handlers";
/**
* Telegram bot webhook. Thin shell over
* `@/lib/telegram-webhook-handlers`: per-source rate limit FIRST, then
* the shared-secret check (constant-time compare), then a bounded JSON
* parse, then dispatch to the callback / free-text handlers. Invalid
* payloads acknowledge with 200 so Telegram does not retry-loop them.
*/
function hasValidSecret(request: NextRequest): boolean {
const expected = process.env.TELEGRAM_WEBHOOK_SECRET;
if (!expected) {
getEvent()?.addWarning("TELEGRAM_WEBHOOK_SECRET not configured");
return false;
}
const received = request.headers.get("x-telegram-bot-api-secret-token");
if (!received) return false;
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(received, "utf8");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
export const POST = apiHandler(async (request: NextRequest) => {
annotate({ action: { name: "telegram.webhook" } });
const ip = getClientIp(request);
const rl = await checkRateLimit(`telegram-webhook:${ip}`, 120, 60 * 1000);
if (!rl.allowed) {
return NextResponse.json(
{ status: "rate_limited" },
{ status: 429, headers: rateLimitHeaders(rl) },
);
}
if (!hasValidSecret(request)) {
return NextResponse.json({ status: "unauthorized" }, { status: 401 });
}
getEvent()?.setAuth({ auth_method: "telegram_webhook" });
let update: TelegramUpdate;
try {
const raw = await request.text();
if (raw.length > 256 * 1024) {
// Oversized payload — acknowledge with 200 like invalid JSON so the
// sender does not retry-loop the update.
return NextResponse.json({ status: "invalid json" }, { status: 200 });
}
update = JSON.parse(raw) as TelegramUpdate;
} catch {
return NextResponse.json({ status: "invalid json" }, { status: 200 });
}
if (!update || typeof update.update_id !== "number") {
return NextResponse.json({ status: "ignored" }, { status: 200 });
}
annotate({ meta: { update_id: update.update_id } });
if (update.callback_query) {
await handleCallback(update);
return NextResponse.json({ status: "ok" }, { status: 200 });
}
if (update.message?.text) {
await handleTextMessage(update);
return NextResponse.json({ status: "ok" }, { status: 200 });
}
return NextResponse.json({ status: "ignored" }, { status: 200 });
});
export const GET = apiHandler(async (request: NextRequest) => {
annotate({ action: { name: "telegram.webhook.verify" } });
if (!hasValidSecret(request)) {
return NextResponse.json({ status: "unauthorized" }, { status: 401 });
}
getEvent()?.setAuth({ auth_method: "telegram_webhook" });
return NextResponse.json({ status: "ok" }, { status: 200 });
});