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:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"ai"
|
||||
],
|
||||
"dependencies": {
|
||||
"hono": "^4.11.7",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
134
src/index.ts
134
src/index.ts
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user