feat(feeds): add configurable per-feed lifetime (TTL)

Replace the demo nightly KV wipe with a per-feed expiry. Feeds can be
given a lifetime at creation (and edited later); FEED_TTL_HOURS locks the
value server-side and greys out the UI field. Expired feeds stay visible
in admin (greyed, actions disabled), return 410 on rss/atom/entries, and
reject inbound emails. The scheduled handler now purges only expired
feeds (KV + R2 attachments) on an hourly global cron.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 09:05:48 +02:00
parent 24c7d2a53e
commit f4d5edda0e
11 changed files with 461 additions and 123 deletions
+17 -5
View File
@@ -1,6 +1,6 @@
import { Context } from "hono";
import { html, raw } from "hono/html";
import { Env, FeedMetadata, EmailData } from "../types";
import { Env, FeedConfig, FeedMetadata, EmailData } from "../types";
import { processEmailContent } from "../utils/html-processor";
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
@@ -13,13 +13,25 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
const emailStorage = c.env.EMAIL_STORAGE;
const feedMetadata = (await emailStorage.get(
`feed:${feedId}:metadata`,
"json",
)) as FeedMetadata | null;
const [feedMetadata, feedConfig] = await Promise.all([
emailStorage.get(
`feed:${feedId}:metadata`,
"json",
) as Promise<FeedMetadata | null>,
emailStorage.get(
`feed:${feedId}:config`,
"json",
) as Promise<FeedConfig | null>,
]);
if (!feedMetadata) {
return new Response("Feed not found", { status: 404 });
}
if (
feedConfig?.expires_at !== undefined &&
feedConfig.expires_at <= Date.now()
) {
return new Response("Feed has expired", { status: 410 });
}
const metaEntry = feedMetadata.emails.find(
(e) => e.receivedAt === receivedAt,