Add a "Public page" link next to the Rendered/Raw toggle in the admin
email view, opening the standalone /entries/:feedId/:entryId render.
Centralize the entry route shape in a pure entryPath() builder, used by
both the admin link and the RSS/Atom/JSON feed generator.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the stacked RSS/Atom URL rows in the dashboard with a compact
"Subscribe" chip block exposing all three feed formats — including JSON
Feed, previously absent from the admin UI. Each chip carries copy, open,
and validate actions; validation links to the W3C Feed Validator (RSS/Atom)
and validator.jsonfeed.org (JSON). The Table view's RSS+Atom columns fold
into a single Formats column.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>