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", "name": "telegram-summary-bot",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"hono": "^4.11.7",
"zod": "^4.3.5" "zod": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
@@ -2625,6 +2626,15 @@
"node": ">= 0.4" "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": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",

View File

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

View File

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