refactor: migrate HTTP routing to Hono framework

- Add hono dependency
- Replace if/else routing chain with Hono app
- Convert all HTTP routes to Hono format:
  - GET /health, /setup-webhook, /webhook-info
  - POST /webhook
  - ALL /api/*
  - GET /
- Keep email, scheduled, queue handlers unchanged
- Maintain 100% backward compatibility

Benefits:
- Cleaner declarative routing
- Type-safe Env bindings
- Ready for future middleware (CORS, rate limiting)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-29 09:40:38 +09:00
parent 41f99334eb
commit 2756dbe804
3 changed files with 77 additions and 68 deletions

10
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "telegram-summary-bot",
"version": "1.0.0",
"dependencies": {
"hono": "^4.11.7",
"zod": "^4.3.5"
},
"devDependencies": {
@@ -2625,6 +2626,15 @@
"node": ">= 0.4"
}
},
"node_modules/hono": {
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",

View File

@@ -32,6 +32,7 @@
"ai"
],
"dependencies": {
"hono": "^4.11.7",
"zod": "^4.3.5"
}
}

View File

@@ -10,80 +10,75 @@ import { handleProvisionQueue, handleProvisionDLQ } from './server-provision';
import { timingSafeEqual } from './security';
import { createLogger } from './utils/logger';
import { notifyAdmin } from './services/notification';
import { Hono } from 'hono';
const logger = createLogger('worker');
export default {
// HTTP 요청 핸들러
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Hono app with Env type
const app = new Hono<{ Bindings: Env }>();
// Webhook 설정 엔드포인트
if (url.pathname === '/setup-webhook') {
if (!env.BOT_TOKEN) {
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
}
if (!env.WEBHOOK_SECRET) {
return Response.json({ error: 'WEBHOOK_SECRET not configured' }, { status: 500 });
}
// Health check (public - minimal info only)
app.get('/health', () => handleHealthCheck());
// 인증: token + secret 검증
const token = url.searchParams.get('token');
const secret = url.searchParams.get('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return new Response('Unauthorized: Invalid or missing token', { status: 401 });
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return new Response('Unauthorized: Invalid or missing secret', { status: 401 });
}
// Setup webhook (with auth)
app.get('/setup-webhook', async (c) => {
const env = c.env;
if (!env.BOT_TOKEN) {
return c.json({ error: 'BOT_TOKEN not configured' }, 500);
}
if (!env.WEBHOOK_SECRET) {
return c.json({ error: 'WEBHOOK_SECRET not configured' }, 500);
}
const webhookUrl = `${url.origin}/webhook`;
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
return Response.json(result);
}
// 인증: token + secret 검증
const token = c.req.query('token');
const secret = c.req.query('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return c.text('Unauthorized: Invalid or missing token', 401);
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return c.text('Unauthorized: Invalid or missing secret', 401);
}
// Webhook 정보 조회
if (url.pathname === '/webhook-info') {
if (!env.BOT_TOKEN) {
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
}
if (!env.WEBHOOK_SECRET) {
return Response.json({ error: 'WEBHOOK_SECRET not configured' }, { status: 500 });
}
const webhookUrl = `${new URL(c.req.url).origin}/webhook`;
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
return c.json(result);
});
// 인증: token + secret 검증
const token = url.searchParams.get('token');
const secret = url.searchParams.get('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return new Response('Unauthorized: Invalid or missing token', { status: 401 });
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return new Response('Unauthorized: Invalid or missing secret', { status: 401 });
}
// Webhook info
app.get('/webhook-info', async (c) => {
const env = c.env;
if (!env.BOT_TOKEN) {
return c.json({ error: 'BOT_TOKEN not configured' }, 500);
}
if (!env.WEBHOOK_SECRET) {
return c.json({ error: 'WEBHOOK_SECRET not configured' }, 500);
}
const result = await getWebhookInfo(env.BOT_TOKEN);
return Response.json(result);
}
// 인증: token + secret 검증
const token = c.req.query('token');
const secret = c.req.query('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return c.text('Unauthorized: Invalid or missing token', 401);
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return c.text('Unauthorized: Invalid or missing secret', 401);
}
// 헬스 체크 (공개 - 최소 정보만)
if (url.pathname === '/health') {
return handleHealthCheck();
}
const result = await getWebhookInfo(env.BOT_TOKEN);
return c.json(result);
});
// API 엔드포인트 처리
if (url.pathname.startsWith('/api/')) {
return handleApiRequest(request, env, url);
}
// API routes
app.all('/api/*', (c) => handleApiRequest(c.req.raw, c.env, new URL(c.req.url)));
// Telegram Webhook 처리
if (url.pathname === '/webhook') {
return handleWebhook(request, env);
}
// Telegram Webhook
app.post('/webhook', (c) => handleWebhook(c.req.raw, c.env));
// 루트 경로
if (url.pathname === '/') {
return new Response(`
Telegram Rolling Summary Bot
// Root path
app.get('/', (c) => {
return c.text(
`Telegram Rolling Summary Bot
Endpoints:
GET /health - Health check
@@ -91,14 +86,17 @@ Endpoints:
GET /setup-webhook - Configure webhook
POST /webhook - Telegram webhook (authenticated)
Documentation: https://github.com/your-repo
`.trim(), {
headers: { 'Content-Type': 'text/plain' },
});
}
Documentation: https://github.com/your-repo`,
200
);
});
return new Response('Not Found', { status: 404 });
},
// 404 handler
app.notFound((c) => c.text('Not Found', 404));
export default {
// HTTP 요청 핸들러 - Hono handles HTTP
fetch: app.fetch,
// Email 핸들러 (SMS → 메일 → 파싱)
async email(message: EmailMessage, env: Env): Promise<void> {