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",
|
"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",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"ai"
|
"ai"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"hono": "^4.11.7",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/index.ts
96
src/index.ts
@@ -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());
|
||||||
|
|
||||||
|
// Setup webhook (with auth)
|
||||||
|
app.get('/setup-webhook', async (c) => {
|
||||||
|
const env = c.env;
|
||||||
if (!env.BOT_TOKEN) {
|
if (!env.BOT_TOKEN) {
|
||||||
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
|
return c.json({ error: 'BOT_TOKEN not configured' }, 500);
|
||||||
}
|
}
|
||||||
if (!env.WEBHOOK_SECRET) {
|
if (!env.WEBHOOK_SECRET) {
|
||||||
return Response.json({ error: 'WEBHOOK_SECRET not configured' }, { status: 500 });
|
return c.json({ error: 'WEBHOOK_SECRET not configured' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 인증: token + secret 검증
|
// 인증: token + secret 검증
|
||||||
const token = url.searchParams.get('token');
|
const token = c.req.query('token');
|
||||||
const secret = url.searchParams.get('secret');
|
const secret = c.req.query('secret');
|
||||||
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
|
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
|
||||||
return new Response('Unauthorized: Invalid or missing token', { status: 401 });
|
return c.text('Unauthorized: Invalid or missing token', 401);
|
||||||
}
|
}
|
||||||
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
|
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
|
||||||
return new Response('Unauthorized: Invalid or missing secret', { status: 401 });
|
return c.text('Unauthorized: Invalid or missing secret', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookUrl = `${url.origin}/webhook`;
|
const webhookUrl = `${new URL(c.req.url).origin}/webhook`;
|
||||||
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
|
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
|
||||||
return Response.json(result);
|
return c.json(result);
|
||||||
}
|
});
|
||||||
|
|
||||||
// Webhook 정보 조회
|
// Webhook info
|
||||||
if (url.pathname === '/webhook-info') {
|
app.get('/webhook-info', async (c) => {
|
||||||
|
const env = c.env;
|
||||||
if (!env.BOT_TOKEN) {
|
if (!env.BOT_TOKEN) {
|
||||||
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
|
return c.json({ error: 'BOT_TOKEN not configured' }, 500);
|
||||||
}
|
}
|
||||||
if (!env.WEBHOOK_SECRET) {
|
if (!env.WEBHOOK_SECRET) {
|
||||||
return Response.json({ error: 'WEBHOOK_SECRET not configured' }, { status: 500 });
|
return c.json({ error: 'WEBHOOK_SECRET not configured' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 인증: token + secret 검증
|
// 인증: token + secret 검증
|
||||||
const token = url.searchParams.get('token');
|
const token = c.req.query('token');
|
||||||
const secret = url.searchParams.get('secret');
|
const secret = c.req.query('secret');
|
||||||
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
|
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
|
||||||
return new Response('Unauthorized: Invalid or missing token', { status: 401 });
|
return c.text('Unauthorized: Invalid or missing token', 401);
|
||||||
}
|
}
|
||||||
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
|
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
|
||||||
return new Response('Unauthorized: Invalid or missing secret', { status: 401 });
|
return c.text('Unauthorized: Invalid or missing secret', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getWebhookInfo(env.BOT_TOKEN);
|
const result = await getWebhookInfo(env.BOT_TOKEN);
|
||||||
return Response.json(result);
|
return c.json(result);
|
||||||
}
|
});
|
||||||
|
|
||||||
// 헬스 체크 (공개 - 최소 정보만)
|
// API routes
|
||||||
if (url.pathname === '/health') {
|
app.all('/api/*', (c) => handleApiRequest(c.req.raw, c.env, new URL(c.req.url)));
|
||||||
return handleHealthCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 엔드포인트 처리
|
// Telegram Webhook
|
||||||
if (url.pathname.startsWith('/api/')) {
|
app.post('/webhook', (c) => handleWebhook(c.req.raw, c.env));
|
||||||
return handleApiRequest(request, env, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegram Webhook 처리
|
// Root path
|
||||||
if (url.pathname === '/webhook') {
|
app.get('/', (c) => {
|
||||||
return handleWebhook(request, env);
|
return c.text(
|
||||||
}
|
`Telegram Rolling Summary Bot
|
||||||
|
|
||||||
// 루트 경로
|
|
||||||
if (url.pathname === '/') {
|
|
||||||
return new Response(`
|
|
||||||
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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user