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
+18 -11
View File
@@ -10,6 +10,11 @@ import { hubRouter } from "./routes/hub";
import { handleCloudflareEmail } from "./lib/cloudflare-email";
import { Env } from "./types";
import { logger } from "./lib/logger";
import {
listAllFeeds,
purgeExpiredFeeds,
removeFeedsFromListBulk,
} from "./routes/admin/helpers";
import { FORWARD_EMAIL_IPS_CACHE_TTL_MS } from "./config/constants";
type AppEnv = { Bindings: Env };
@@ -176,16 +181,18 @@ export default {
await handleCloudflareEmail(message, env, ctx);
},
async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) {
let cursor: string | undefined;
let deleted = 0;
do {
const result = await env.EMAIL_STORAGE.list({ cursor });
await Promise.all(
result.keys.map(({ name }) => env.EMAIL_STORAGE.delete(name)),
);
deleted += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
logger.info("Demo KV reset complete", { deleted });
const feeds = await listAllFeeds(env.EMAIL_STORAGE);
const now = Date.now();
const expiredIds = feeds
.filter((f) => f.expires_at !== undefined && f.expires_at <= now)
.map((f) => f.id);
for (const feedId of expiredIds) {
await purgeExpiredFeeds(env.EMAIL_STORAGE, feedId, env.ATTACHMENT_BUCKET);
}
if (expiredIds.length > 0) {
await removeFeedsFromListBulk(env.EMAIL_STORAGE, expiredIds);
logger.info("Feed TTL cleanup", { deleted: expiredIds.length });
}
},
};