Commit Graph

25 Commits

Author SHA1 Message Date
Julien Herr f44c6c1eda feat(admin): dashboard pending-confirmation pill 2026-05-25 09:12:27 +02:00
Julien Herr 1525b36cab feat(admin): surface confirmation link, badge, banner + dismiss
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:08:30 +02:00
Julien Herr 2a3aeb8a18 feat(admin): link email detail to its public entry page
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>
2026-05-24 23:26:16 +02:00
Julien Herr b3a979fd03 feat(admin): per-feed Subscribe chips with copy/open/validate for RSS/Atom/JSON
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>
2026-05-24 23:08:27 +02:00
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
Julien Herr 5137637181 feat(attachments): render inline cid images in place, not as attachments
Inline images (referenced by src="cid:…") are now classified at ingest and
kept out of the downloadable attachment lists, RSS/Atom enclosures, and the
API — while still stored in R2 and cleaned up with the email. Fixes the admin
email preview, which injected raw HTML into the data: iframe so cid refs never
resolved; it now rewrites them to absolute /files URLs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 14:39:59 +02:00
Julien Herr 7bf0f71f86 refactor: split src into domain / application / infrastructure layers
Replace the history-driven lib/ + utils/ split with DDD layers:
- domain/: aggregate, repositories, value objects, pure parsers/format
- application/: feed-service, email-processor, feed-fetcher, stats
- infrastructure/: logging, auth, KV/R2 adapters, HTTP, framework glue

Pure file relocation; imports updated mechanically. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 00:46:56 +02:00
Julien Herr c45f6677fe refactor(domain): introduce the Feed aggregate as the write-path API
Add a Feed aggregate class owning config + the email index, with create,
ingest, removeEmails, isExpired and accepts delegating to the existing
pure invariant functions. FeedRepository gains load/save/saveMetadata
that reconstitute and persist the aggregate.

All write paths now go through it: createFeedRecord (Feed.create),
email ingestion (feed.ingest), and every email deletion in the admin UI
and REST API (feed.removeEmails) — no route mutates metadata.emails
directly anymore. KV key strings unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 00:33:14 +02:00
Julien Herr 3368b0d1d2 feat(admin): collapse create-feed form into accordion
Wrap the "Create New Feed" form in a native <details> accordion, collapsed
by default and auto-opened when no feeds exist. After creating a feed,
redirect to the "Your Feeds" anchor so the new feed is immediately visible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:41:25 +02:00
Julien Herr 6cd2d425a2 feat(attachments): list downloadable attachments on admin email detail page
The admin email detail view loaded the full email but never rendered its
attachments, so there was no way to download them from the admin UI (only
the public entry view and the feed enclosure exposed them).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:11:29 +02:00
Julien Herr 9141cf89bd fix(attachments): purge R2 attachments on no-JS bulk email delete
The form-based bulk-delete fallback removed KV entries but left R2
attachments orphaned. Extract a shared deleteAttachmentsForEmails helper
and use it across single, JSON bulk, and form bulk delete paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:57:31 +02:00
Julien Herr 7226e718f7 feat(admin): paperclip indicator for emails with attachments
Show an inline paperclip icon before the subject in the admin email
list when an email has attachments, with the count in a tooltip. Uses
the attachmentIds already stored in metadata, so no extra fetch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:10:24 +02:00
Julien Herr 3ad0188bc0 feat(unsubscribe): RFC 8058 one-click unsubscribe on feed deletion
Capture each sender's List-Unsubscribe one-click URL during ingestion
(stored per sender in feed metadata, mirroring the iconDomain pattern) and
fire one-click POSTs via ctx.waitUntil when a feed is deleted, so newsletters
stop mailing the now-dead address. Tracked with a new unsubscribes_sent
counter surfaced on the status page and /api/stats.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 14:35:05 +02:00
Julien Herr 4a4c276859 feat: add sender blocklist with priority matching and quick-add dropdown
- Add `blocked_senders` field to FeedConfig (alongside existing `allowed_senders`)
- Refactor sender matching to priority-based logic: exact block > exact allow > domain block > domain allow, enabling exceptions (e.g. allow toto@gmail.com despite blocking gmail.com)
- Add `POST /admin/feeds/:feedId/sender-filter` endpoint for quick allow/block from email detail view; returns 409 on conflict with opposite list
- Add ⋮ dropdown on From field in email detail with 4 options (allow/block sender/domain), inline success/error feedback
- Add blocked_senders textarea to create/edit feed forms
- 209 tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-22 23:09:53 +02:00
Julien Herr a0415cdc41 refactor: replace custom HMAC CSRF with hono/csrf middleware
Removes 38-line hand-rolled HMAC-SHA256 implementation in favour of
the built-in hono/csrf, which validates the Origin header natively.

- Delete src/utils/csrf.ts
- Replace custom CSRF middleware with hono/csrf (Origin-header check)
- Remove csrfToken from ContextVariableMap, layout(), forms, and JS fetch() calls
- Update admin tests: swap X-CSRF-Token for Origin header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:28:26 +02:00
Julien Herr 7d375693b9 feat: complete Phase 2 tech debt remediation
- Extract shared RSS/Atom fetch logic into feed-fetcher utility (P1-3)
- Split email-processor into validateEmail/storeEmail functions (P1-6)
- Add stateless HMAC-SHA256 CSRF protection to admin forms (P2-8)
- Fix Hono<{ Bindings: Env }> type safety across all routes (P3-13)
- Add entries.test.ts and files.test.ts with full coverage (P1-7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 09:46:55 +02:00
Julien Herr 5723fd36f9 refactor(admin): validate JSON feed update via @hono/zod-validator
Moves validation of POST /api/feeds/:feedId/update from inline
schema.parse() to zValidator middleware. The route now receives
typed validated data via c.req.valid("json"), and returns a
structured {success: false, error: ZodIssue[]} on invalid input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:46:51 +02:00
Julien Herr b24ee969d1 style: fix Prettier formatting on 11 files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:35:37 +02:00
Julien Herr caaa6a7ba6 feat: add external proxy auth support (Authelia/Authentik)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 08:39:10 +02:00
Julien Herr 29446a2aac chore: add typecheck script and fix pre-existing TypeScript errors
- Add `typecheck` script (`tsc --noEmit`) to package.json
- Remove conflicting `declare global` from test/setup.ts (superseded
  by @cloudflare/workers-types); use `globalThis as any` for test globals
  and declare minimal `require` locally to avoid pulling in @types/node
- Cast `createMockEnv()` and `deleteRes.json()` results in admin.test.ts
  to silence strict `unknown` / MockKV-vs-KVNamespace errors

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-20 22:54:32 +02:00
Julien Herr 3ed9d2ee22 chore: apply Prettier formatting to entire codebase
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-20 22:01:53 +02:00
Young Lee bf3a4d9672 Improve admin delete confirmations 2026-02-06 13:36:17 -08:00
Young Lee 223560e874 fix(security): lock down admin + add bulk cleanup UI 2026-02-05 23:18:25 -08:00
Young Lee daf54a0fc0 chore: modernize setup, dependencies, and project docs 2026-02-05 22:34:13 -08:00
Young Lee 6e546d31a0 Testing 2026-02-05 22:18:29 -08:00