Files
kill-the-news/src/domain/feed-keys.ts
T
Julien Herr 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 <noreply@anthropic.com>
2026-05-24 22:46:37 +02:00

44 lines
1.7 KiB
TypeScript

import { FEEDS_LIST_KEY, STATS_KEY } from "../config/constants";
/**
* The KV key schema, in one pure place. Every repository builds its keys here so
* the wire format lives in a single module — never inline a `feed:`/`icon:`/
* `websub:` string elsewhere. Strings are byte-identical to the original schema;
* changing them would require migrating live KV data.
*/
const WEBSUB_PREFIX = "websub:subs:";
export const feedKeys = {
config: (feedId: string): string => `feed:${feedId}:config`,
metadata: (feedId: string): string => `feed:${feedId}:metadata`,
/** Secondary index: inbound mailbox local part → feed id (resolved at reception). */
inbound: (mailboxId: string): string => `inbound:${mailboxId}`,
/** Prefix covering every key owned by a feed (config, metadata, emails). */
feedPrefix: (feedId: string): string => `feed:${feedId}:`,
/** Mint a fresh, time-ordered email key. Call once and reuse the result. */
newEmail: (feedId: string): string => `feed:${feedId}:${Date.now()}`,
/** KV key for a domain's cached favicon (shared across feeds). */
icon: (domain: string): string => `icon:${domain}`,
websub: (feedId: string): string => `${WEBSUB_PREFIX}${feedId}`,
/** Prefix matching every per-feed WebSub subscription key. */
websubPrefix: (): string => WEBSUB_PREFIX,
/** True when `key` is an email entry (not the feed's config/metadata key). */
isEmail: (feedId: string, key: string): boolean => {
const suffix = key.slice(feedKeys.feedPrefix(feedId).length);
return suffix !== "config" && suffix !== "metadata";
},
/** Recover the feed id embedded in an email key (`feed:<id>:<ts>`). */
feedIdFromEmail: (key: string): string => key.split(":")[1],
} as const;
export { FEEDS_LIST_KEY, STATS_KEY };