Files
kill-the-news/src/application/feed-fetcher.ts
T
Julien HerrandClaude Opus 4.7 1a4a479190 feat: decouple read FeedId from inbound MailboxId
Separate the two feed identities so the public read URL never reveals the
inbound address and vice-versa:

- FeedId becomes an opaque high-entropy token (read id + KV key); MailboxId
  (noun.noun.NN) owns the inbound address and the untrusted-input boundary
  via MailboxId.parse. They map only through the inbound:<mailbox> secondary
  index, resolved solely at reception.
- inbound index lifecycle is owned by FeedRepository: written by save/saveConfig,
  dropped by removeFromList(Bulk) — symmetric, never mirrored by hand (removes the
  manual delete in feed-service + the cron loop, and a silent empty-catch).
- Feed.mailboxId exposes a MailboxId VO (symmetry with Feed.id); the
  mailbox@domain shape lives on MailboxId.emailAddress(domain).
- Distinguish mailbox_unknown (no feed claims the address) from feed_not_found
  (dangling index) for observability; both forwardable, both 404.
- Drop the redundant EmailParser.extractMailbox pass-through so MailboxId.parse
  is the single parse boundary.

Docs (README/INSTALL/CLAUDE.md/landing) and tests updated; 439 tests green,
tsc clean, build dry-run OK.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 22:46:37 +02:00

39 lines
1.2 KiB
TypeScript

import { Env, FeedConfig, EmailData } from "../types";
import { MAX_FEED_ITEMS } from "../config/constants";
import { FeedRepository } from "../infrastructure/feed-repository";
import { FeedId } from "../domain/value-objects/feed-id";
export interface FeedData {
feedConfig: FeedConfig;
emails: EmailData[];
}
export async function fetchFeedData(
feedId: FeedId,
env: Env,
): Promise<FeedData | null> {
const repo = FeedRepository.from(env);
const feedMetadata = await repo.getMetadata(feedId);
if (!feedMetadata) return null;
const feedConfig = (await repo.getConfig(feedId)) ?? {
title: `Newsletter Feed ${feedId.value}`,
description: "Converted email newsletter",
language: "en",
// Read-model fallback only: the RSS/Atom/JSON path never builds the inbound
// address, so an empty mailbox is inert here (the real one lives on config).
mailbox_id: "",
created_at: Date.now(),
};
const emailRefs = feedMetadata.emails.slice(0, MAX_FEED_ITEMS);
const emails: EmailData[] = [];
for (const ref of emailRefs) {
const data = await repo.getEmail(ref.key);
if (data) emails.push(data);
}
return { feedConfig, emails };
}