mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
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>
This commit is contained in:
@@ -50,7 +50,9 @@ Gaps found by reading every open/closed issue + PR on [kill-the-newsletter](http
|
||||
|
||||
- [ ] `P1·M` **Subscription confirmation handling** — _the single most recurring upstream request_ ([#5](https://github.com/leafac/kill-the-newsletter/issues/5), [#23](https://github.com/leafac/kill-the-newsletter/issues/23), [#57](https://github.com/leafac/kill-the-newsletter/issues/57), [#73](https://github.com/leafac/kill-the-newsletter/issues/73), [#89](https://github.com/leafac/kill-the-newsletter/issues/89), [#95](https://github.com/leafac/kill-the-newsletter/issues/95), [#97](https://github.com/leafac/kill-the-newsletter/issues/97)). Newsletters require a "click to confirm your email" step; users can't easily find/click the link buried in a feed reader. Our admin already lists emails, but nothing **surfaces** the confirmation link or shows the first email inline right after feed creation. Low effort, high payoff (admin UX in `src/routes/admin/feeds.tsx` + maybe extract candidate confirm links during ingestion in `src/application/email-processor.ts`).
|
||||
|
||||
- [ ] `P1·M` **Separate write (email) / read (feed) IDs** — _most-requested privacy gap, still open upstream_ ([#114](https://github.com/leafac/kill-the-newsletter/issues/114), [#93](https://github.com/leafac/kill-the-newsletter/issues/93), [#75](https://github.com/leafac/kill-the-newsletter/issues/75)). Today `feedEmailAddress = <feedId>@domain` and `/rss/<feedId>` reuse the **same** id (`src/infrastructure/urls.ts`), so anyone with the inbound address can read the feed (and vice-versa) — you can't share a feed without leaking its subscribe address. Add a distinct read id alongside the write id: touch `FeedState`, id generation (`FeedId`), `urls.ts`, the `inbound` parse, and the feed-list/registry. Medium effort.
|
||||
- [x] `P1·M` **Separate write (email) / read (feed) IDs** — _most-requested privacy gap, still open upstream_ ([#114](https://github.com/leafac/kill-the-newsletter/issues/114), [#93](https://github.com/leafac/kill-the-newsletter/issues/93), [#75](https://github.com/leafac/kill-the-newsletter/issues/75)). The two identities are now decoupled: `FeedId` is an **opaque random token** (`FeedId.generate()` → 22-char base64url) used as the KV storage key and the public read id (`/rss/:feedId`), while the inbound address is a separate `MailboxId` VO (`noun.noun.NN`, the old format) resolved to its feed **only at reception** via a new `inbound:<mailboxId>` secondary index (`src/infrastructure/feed-repository.ts` `resolveInbound`). `MailboxId.parse` owns the untrusted-input boundary (moved off `FeedId`); the mailbox lives on `FeedState.mailboxId` / `mailbox_id` and is projected into `feeds:list`. Reading `/rss/<noun.noun.NN>` 404s and no public feed output contains the inbound address. Pre-release, so no migration/backward-compat. — _origin: [ktn#114](https://github.com/leafac/kill-the-newsletter/issues/114), [ktn#93](https://github.com/leafac/kill-the-newsletter/issues/93), [ktn#75](https://github.com/leafac/kill-the-newsletter/issues/75)_
|
||||
|
||||
- [ ] `P2·S` **Rotate the inbound mailbox and/or feed id** — _follow-up to the write/read separation above_. Now that the inbound address (`MailboxId`) and the read id (`FeedId`) are decoupled, offer an admin + REST action to **re-mint** either one to revoke a leaked subscribe address or a shared feed URL. Rotating the mailbox: generate a new `MailboxId`, write the new `inbound:<new>` index, delete the old; rotating the read id is heavier (it's the KV storage key — would require re-keying `feed:<id>:*`, so prefer rotating only the mailbox first). Touch `feed-service.ts`, `feed-repository.ts`, admin UI, `api/index.ts`. — _origin: internal (privacy)_
|
||||
|
||||
- [ ] `P2·M` **Proxy/prefetch remote images** ([#69](https://github.com/leafac/kill-the-newsletter/issues/69)). We already proxy inline `cid:` images via R2, but remote `<img src="https://…">` stay remote → tracking pixels fire on read. Extend `src/infrastructure/html-processor.ts` to rewrite remote image src through a worker proxy/cache endpoint (reuse the R2 + Cache API pattern from favicons).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user