feat: complete Phase 2 tech debt remediation

- Extract shared RSS/Atom fetch logic into feed-fetcher utility (P1-3)
- Split email-processor into validateEmail/storeEmail functions (P1-6)
- Add stateless HMAC-SHA256 CSRF protection to admin forms (P2-8)
- Fix Hono<{ Bindings: Env }> type safety across all routes (P3-13)
- Add entries.test.ts and files.test.ts with full coverage (P1-7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-22 09:46:55 +02:00
parent f2981eec31
commit 7d375693b9
15 changed files with 485 additions and 152 deletions
+15 -58
View File
@@ -1,80 +1,37 @@
import { Context } from "hono";
import { Env, FeedConfig, FeedMetadata, EmailData } from "../types";
import { Env } from "../types";
import { generateRssFeed } from "../utils/feed-generator";
import { fetchFeedData } from "../utils/feed-fetcher";
/**
* Generates an RSS feed for a specific feed ID
*/
export async function handle(c: Context): Promise<Response> {
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
try {
// Type assertion for environment variables
const env = c.env as unknown as Env;
// Extract the feed ID from the route params
const feedId = c.req.param("feedId");
if (!feedId) {
return new Response("Feed ID is required", { status: 400 });
}
// Get the KV namespace
const emailStorage = env.EMAIL_STORAGE;
// Check if the feed exists
const feedMetadataKey = `feed:${feedId}:metadata`;
const feedMetadata = (await emailStorage.get(
feedMetadataKey,
"json",
)) as FeedMetadata | null;
if (!feedMetadata) {
const feedData = await fetchFeedData(feedId, c.env, "rss");
if (!feedData) {
return new Response("Feed not found", { status: 404 });
}
// Get feed configuration (title, description, etc.)
const feedConfigKey = `feed:${feedId}:config`;
const feedConfig = ((await emailStorage.get(
feedConfigKey,
"json",
)) as FeedConfig | null) || {
title: `Newsletter Feed ${feedId}`,
description: "Converted email newsletter",
site_url: `https://${env.DOMAIN}/rss/${feedId}`,
feed_url: `https://${env.DOMAIN}/rss/${feedId}`,
language: "en",
created_at: Date.now(),
};
// Get the emails for this feed (up to the last 20)
const emails = feedMetadata.emails.slice(0, 20);
const emailsData: EmailData[] = [];
// Fetch all email content
for (const email of emails) {
const emailData = (await emailStorage.get(
email.key,
"json",
)) as EmailData | null;
if (emailData) {
emailsData.push(emailData);
}
}
// Generate the RSS feed XML
const baseUrl = `https://${env.DOMAIN}`;
const rssXml = generateRssFeed(feedConfig, emailsData, baseUrl, feedId);
// Return the RSS feed with appropriate content type
const baseUrl = `https://${c.env.DOMAIN}`;
const rssXml = generateRssFeed(
feedData.feedConfig,
feedData.emails,
baseUrl,
feedId,
);
const linkHeader = [
`<https://${env.DOMAIN}/hub>; rel="hub"`,
`<https://${env.DOMAIN}/rss/${feedId}>; rel="self"`,
`<${baseUrl}/hub>; rel="hub"`,
`<${baseUrl}/rss/${feedId}>; rel="self"`,
].join(", ");
return new Response(rssXml, {
status: 200,
headers: {
"Content-Type": "application/rss+xml",
"Cache-Control": "max-age=1800", // 30 minutes cache
"Cache-Control": "max-age=1800",
Link: linkHeader,
},
});