Commit Graph
100 Commits
Author SHA1 Message Date
Julien HerrandClaude Opus 4.7 fd6a1a945f feat(landing): show animated live demo stats counters
Add a "Live from the demo instance" section to the landing page that
fetches feeds_created and emails_received from the demo /api/stats and
counts them up on scroll into view. Make /api/stats publicly readable
(CORS *) and refresh the stale allowlist origins to kill-the.news.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 10:09:13 +02:00
Julien HerrandClaude Opus 4.7 d1959acad1 docs(todo): add favicon and RFC 8058 unsubscribe ideas
Add backlog items for a project favicon (also used as the per-feed
fallback), per-feed favicons resolved from the last sender's domain with
aggressive caching, and RFC 8058 one-click unsubscribe on feed deletion.
Include a detailed design breakdown for the per-feed favicon feature.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:57:59 +02:00
Julien HerrandClaude Opus 4.7 b534ce5bf8 feat(monitoring): add stats counters API and public status page
Add GET /api/stats exposing cumulative counters (feeds created/deleted,
emails received/rejected, recent date-times) plus live values (active
feeds, active WebSub subscriptions). Counters persist in a stats:counters
KV singleton and are incremented at the email-processing chokepoint and
feed create/delete paths. Replace the / → /admin redirect with a public
status page rendering these figures with a link to the admin.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:50:51 +02:00
Julien HerrandClaude Sonnet 4.6 f4d5edda0e feat(feeds): add configurable per-feed lifetime (TTL)
Replace the demo nightly KV wipe with a per-feed expiry. Feeds can be
given a lifetime at creation (and edited later); FEED_TTL_HOURS locks the
value server-side and greys out the UI field. Expired feeds stay visible
in admin (greyed, actions disabled), return 410 on rss/atom/entries, and
reject inbound emails. The scheduled handler now purges only expired
feeds (KV + R2 attachments) on an hourly global cron.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-23 09:05:48 +02:00
Julien HerrandClaude Sonnet 4.6 24c7d2a53e fix(release): create release before uploading bundle
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:50:39 +02:00
Julien HerrandClaude Sonnet 4.6 7e72022ffc fix(release): remove redundant cp and use env var for bundle path
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:49:25 +02:00
Julien HerrandClaude Sonnet 4.6 78117421fd fix(build): add wrangler.build.toml for CI and fix autoconfig
Wrangler 4.94+ introduced --experimental-autoconfig (default: true) which
fails in non-interactive CI environments. Without a committed wrangler.toml,
the release action build was broken.

- Add wrangler.build.toml with minimal config (placeholder KV ID, no secrets)
- Update build script to use wrangler.build.toml + --no-experimental-autoconfig

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:47:48 +02:00
Julien HerrandClaude Sonnet 4.6 1b31665b4f docs: update WAF rate limiting instructions for free tier accuracy
- Fix menu path: Security → Security rules (not Security → WAF)
- Add free tier limitations note: 1 rule max, 10s period/block cap
- Show recommended vs free tier limits side by side in table
- Remove HTTP method filter from conditions (not available in rate limiting rules)
- Note Terraform supports method filtering and longer periods (paid plan)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:38:41 +02:00
Julien HerrandClaude Sonnet 4.6 75a557d542 feat(admin): add site header and footer to all admin pages
Adds a minimal header with a branded link to kill-the.news and an
"admin" badge, plus a discreet footer with site link and GitHub
Sponsors link.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:23:48 +02:00
Julien HerrandClaude Sonnet 4.5 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 [email protected] 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 <[email protected]>
2026-05-22 23:09:53 +02:00
Julien HerrandClaude Sonnet 4.6 7b2b98d693 refactor: extract url helpers, add EMAIL_DOMAIN support
- Add src/utils/urls.ts with baseUrl, feedRssUrl, feedAtomUrl, feedUrl,
  feedEmailAddress, feedTopicPattern
- Add optional EMAIL_DOMAIN env var so web domain and email domain can
  differ (e.g. demo.kill-the.news serves feeds, @kill-the.news receives mail)
- Replace all inline domain template literals with the new helpers
- Remove unused site_url/feed_url fields from FeedConfig
- Remove unused feedPath param from fetchFeedData
- Extract verifyCallback() to deduplicate verifyAndStoreSubscription /
  verifyAndDeleteSubscription

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 22:38:29 +02:00
Julien HerrandClaude Sonnet 4.6 c64ceb4ee7 feat(site): add GitHub Sponsors links (nav, sponsor section, footer)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 21:56:56 +02:00
Julien HerrandClaude Sonnet 4.6 6bf5ae0356 feat: landing page install guide, demo banner, WAF docs, nightly demo reset
- docs/index.html: nav links (Features/How it works/Install), hero CTAs
  (Try demo primary, Self-host, GitHub), demo banner with credentials,
  full 7-step installation section with WAF rate limiting guide (dashboard
  + Terraform) integrated as step 7
- wrangler-example.toml: cron trigger on demo env for nightly KV reset at 03:00 UTC
- src/index.ts: scheduled handler that wipes all EMAIL_STORAGE KV keys
- TODO.md: mark WAF rate limiting as done

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 21:50:42 +02:00
Julien Herr af721c081a chore: mark WebSub as done in TODO.md 2026-05-22 21:14:00 +02:00
Julien HerrandClaude Sonnet 4.5 a29e9ab372 feat: WebSub Atom support, HTML processing via linkedom, W3C badges
WebSub / PubSubHubbub:
- Hub now accepts both /rss/:id and /atom/:id topic URLs
- WebSubSubscription stores format ("rss" | "atom")
- notifySubscribers sends RSS or Atom XML with correct Content-Type
- verifyAndStoreSubscription sends correct topic URL per format
- CI paths-ignore docs/** to skip deploy on docs-only changes

HTML processing (linkedom + escape-html):
- New html-processor.ts: body extraction, script/iframe/object removal,
  event handler + javascript: URL stripping, mso-* style cleanup,
  plain text → <pre> with HTML escaping via escape-html
- feed-generator.ts and entries.ts use processEmailContent

Admin UI:
- W3C validation badges (Atom + RSS) on feed detail page

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-05-22 21:12:22 +02:00
Julien HerrandClaude Sonnet 4.6 1789870f27 fix(feed): use permalink URL as Atom entry id, strip mso-* inline styles
- Entry <id> was a non-URL string (timestamp + base64 snippet), which
  is invalid per the Atom spec; now uses the entry permalink URL which
  is both valid and stable across feed regeneration
- Strip mso-* properties from inline style attributes in extracted body
  content to eliminate the feed validator DangerousStyleAttr warning
  caused by Microsoft Office HTML in newsletter emails

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 21:12:22 +02:00
Julien HerrandClaude Sonnet 4.6 afed4464cf fix(feed): use permalink URL as Atom entry id, strip mso-* inline styles
- Entry <id> was a non-URL string (timestamp + base64 snippet), which
  is invalid per the Atom spec; now uses the entry permalink URL which
  is both valid and stable across feed regeneration
- Strip mso-* properties from inline style attributes in extracted body
  content to eliminate the feed validator DangerousStyleAttr warning
  caused by Microsoft Office HTML in newsletter emails

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:43:06 +02:00
Julien HerrandClaude Sonnet 4.6 4428f35dd4 fix(feed): add Atom link in emails page, fix HTML stripping, use request URL for self-link
- Add Atom Feed URL to the Feed Details card in the emails page
- Fix extractBodyContent to handle emails without a closing </body> tag
  (regex now falls back to capturing everything after the opening <body>)
- Use the actual request URL origin for atom:link rel="self" in RSS/Atom
  feeds, guaranteeing it always matches the document location regardless
  of how DOMAIN is configured

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:41:21 +02:00
Julien HerrandClaude Sonnet 4.6 bcc9640591 fix(feed): correct feed link, canonical id, and strip html wrapper from content
- link: computed as /admin/feeds/:id/emails instead of stale site_url from KV
- id: computed dynamically from baseUrl instead of stale feed_url from KV
- item description/content: strip <html><head><body> wrapper via extractBodyContent()
  so feed readers receive a body fragment, not a full HTML document

Fixes RSS validator warnings: SelfDoesntMatchLocation (stale KV domain) and
InvalidHTML (full HTML document inside <description>/<content:encoded>).
Adds 8 tests covering extractBodyContent and the new feed/atom link assertions.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:28:13 +02:00
Julien HerrandClaude Sonnet 4.6 780bf6c190 fix(admin): add Atom feed URL, unify container width, update branding
- Atom feed URL shown in both list and table views (new Atom column)
- Remove container-wide toggle — both views now use max-width 1200px
- Update dashboard title and login title to kill-the-news

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:21:49 +02:00
Julien HerrandClaude Sonnet 4.6 41f70143d1 design(admin): add Inter font, update page title to kill-the-news
Imports Inter 400/500/600/700 from Google Fonts to match the landing
page typography. Updates browser tab title format.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:21:42 +02:00
Julien HerrandClaude Sonnet 4.6 b95fbb468f design(admin): align with kill-the-news landing page visual identity
- variables.css: orange primary (#f6821f), dark bg (#0a0a0a), Inter font
- layout.css: orange radial glow, unified container 1200px (no width jump)
- components.css: orange buttons, remove backdrop-filter on inputs/cards

Fixes blurred form fields (double backdrop-filter), jarring width shift
between list/table views, and mismatched blue iOS aesthetic vs orange
Cloudflare identity of the site.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:21:34 +02:00
Julien HerrandClaude Sonnet 4.6 ba7add5f53 fix(demo): correct DOMAIN env var to demo.kill-the.news
The [env.demo] section pointed DOMAIN to kill-the.news while the
custom_domain route was demo.kill-the.news, causing feed/email URLs
to show the wrong domain in the admin UI.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 18:21:26 +02:00
Julien Herr 3ccbd876b6 build(deps-dev): bump dev-dependencies group with 8 updates
Includes TypeScript 5→6 migration: moduleResolution node→bundler.
2026-05-22 17:35:01 +02:00
Julien Herr 03d069bfa3 build(deps): bump hono from 4.11.7 to 4.12.22
Fix TypeScript errors from stricter c.req.param() types (string | undefined).
2026-05-22 17:33:47 +02:00
Julien Herr ea400883dd build(deps): bump zod from 4.3.6 to 4.4.3 2026-05-22 17:32:50 +02:00
Julien Herr 7dd1225cde build(deps): bump feed from 5.2.0 to 5.2.1 2026-05-22 17:32:37 +02:00
Julien Herr 3b4a1ce98c build(deps): bump actions/setup-node from v4 to v6.4.0 2026-05-22 17:32:19 +02:00
Julien Herr 3fa17228a3 build(deps): bump actions/checkout from v4 to v6.0.2 2026-05-22 17:32:04 +02:00
Julien Herr ebfb8d4a60 fix(config): silence CSS module rule fallthrough warning 2026-05-22 17:28:02 +02:00
Julien Herr d657875322 feat(ci): auto-deploy demo instance on main push 2026-05-22 17:20:46 +02:00
Julien Herr 4d9418057e feat(deploy): add demo environment to wrangler config 2026-05-22 17:20:08 +02:00
Julien HerrandClaude Sonnet 4.6 edc1183e61 docs: rewrite CLAUDE.md to match actual codebase, remove .cursor
CLAUDE.md now reflects the real route set (atom, entries, files, hub,
email handler), src/lib/ layout, admin sub-modules, client script
pipeline, full Env bindings, and WebSub KV schema.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 15:53:22 +02:00
Julien HerrandClaude Sonnet 4.6 4316354ee5 refactor(styles): migrate TS template literal styles to real CSS files
Extract CSS from TypeScript template literals into standalone .css files
(variables.css, layout.css, components.css, utilities.css) and update
src/routes/admin/ui.tsx to import them directly via Wrangler text imports,
concatenating the strings at runtime for the inline <style> tag.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 15:44:08 +02:00
Julien HerrandClaude Sonnet 4.6 5d75682702 build: add Wrangler text rule for CSS imports
Add [[rules]] type = "Text" globs = ["**/*.css"] to wrangler-example.toml
so Wrangler bundles .css files as raw text strings importable in TypeScript.
Add src/types/css.d.ts to provide the module declaration for `import css from "*.css"`.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 15:43:49 +02:00
Julien HerrandClaude Sonnet 4.5 d64f703820 fix(ui): replace children cast to any with hono/jsx Child type
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-05-22 15:35:10 +02:00
Julien HerrandClaude Sonnet 4.5 0e1592f8c6 build: gitignore generated client scripts, rebuild on npm install
Move src/scripts/generated/ to .gitignore — files are deterministic
build artefacts and don't belong in version control. Wire build:client
into prepare so they're regenerated automatically after npm install.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-05-22 13:53:33 +02:00
Julien HerrandClaude Sonnet 4.6 0f6670d0e9 refactor(admin): extract emailsScript to src/scripts/client/emails-page.ts
Moves the inline JS template literal from emails.tsx into a typed
TypeScript source file. The dynamic feedId value (previously interpolated
directly) is now passed via a window.__APP_CONFIG__ bootstrap script
injected immediately before the compiled static script in the HTML.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:50:54 +02:00
Julien HerrandClaude Sonnet 4.6 40f4b42cd5 refactor(admin): extract dashboardScript to src/scripts/client/dashboard.ts
Moves the 650-line inline JS template literal from admin.tsx into a
proper TypeScript source file with full type annotations. esbuild
compiles it to a minified IIFE committed in src/scripts/generated/,
which is imported and inlined into the HTML response as before.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:50:47 +02:00
Julien HerrandClaude Sonnet 4.6 0663861471 build: add client-side TypeScript compilation pipeline
Adds scripts/build-client.mjs which uses esbuild to compile TypeScript
files in src/scripts/client/ into minified IIFE bundles, then writes
them as TypeScript string-constant modules in src/scripts/generated/.

- Adds build:client npm script; wires it as prebuild and predev hooks
- Adds src/scripts/client/tsconfig.json with DOM lib for IDE support
- Excludes src/scripts/client/ from the root Worker tsconfig to avoid
  DOM type conflicts with the Cloudflare Workers runtime types

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:50:41 +02:00
Julien HerrandClaude Sonnet 4.6 671179acc7 build: add esbuild dev dependency
Installs esbuild to support compiling client-side TypeScript into
minified IIFE bundles that can be inlined into Worker HTML responses.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:50:31 +02:00
Julien HerrandClaude Sonnet 4.6 9bfebf4aa4 refactor(admin): migrate admin.ts to admin.tsx with JSX login and dashboard
Convert login page and dashboard GET routes from hono/html tagged template
literals to typed JSX using the <Layout> component. Extracts reusable
CopyIcon, CheckIcon, and CopyFieldInline components. Dashboard inline
script (~650 lines) preserved exactly via dangerouslySetInnerHTML constant.
All auth logic, CSRF middleware, and API routes are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:17:46 +02:00
Julien HerrandClaude Sonnet 4.6 c9ab3839e4 refactor(admin): migrate emails.ts to emails.tsx with JSX rendering
Convert feed emails list and single email view GET routes from hono/html
tagged template literals to typed JSX. Extracts reusable CopyField and
SVG icon components. Inline page scripts are preserved verbatim via
dangerouslySetInnerHTML. Raw HTML display in single email view uses
dangerouslySetInnerHTML to avoid double-escaping pre-escaped content.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:17:38 +02:00
Julien HerrandClaude Sonnet 4.6 ecb85730e0 refactor(admin): migrate feeds.ts to feeds.tsx with JSX rendering
Convert the edit feed GET route from hono/html tagged template literals
to typed JSX using the <Layout> component. All CRUD routes and business
logic are preserved unchanged. textarea placeholder special characters
are now handled via JSX attribute escaping rather than &#10; entities.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:17:31 +02:00
Julien HerrandClaude Sonnet 4.6 996aa5e211 refactor(admin): migrate ui.ts to ui.tsx with JSX Layout component
Replace hono/html tagged template layout() function with a typed JSX
<Layout> component. CSS and interactive scripts are injected via
dangerouslySetInnerHTML to preserve exact output. clampText() is
preserved and re-exported for consumers.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:17:24 +02:00
Julien HerrandClaude Sonnet 4.6 f5bac1bd7d build: add hono/jsx support to tsconfig
Add jsx and jsxImportSource compiler options to enable hono/jsx server-side
rendering in .tsx files without per-file JSX pragmas.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 13:17:16 +02:00
Julien HerrandClaude Sonnet 4.6 d82344ac67 chore: remove TECH_DEBT.md — all tasks completed
All three phases fully remediated. P1-5 (WAF rate limiting) is
infrastructure-only and tracked in TODO.md.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 12:01:42 +02:00
Julien HerrandClaude Sonnet 4.6 427eac973c docs: mark Phase 3 tasks done; document WAF rate limiting in TODO
P1-4, P2-10, P2-11 marked DONE in TECH_DEBT.md.
P1-5 (rate limiting) is infrastructure-only — documented in TODO.md with
the recommended Cloudflare WAF rule targets and thresholds.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 10:56:55 +02:00
Julien HerrandClaude Sonnet 4.6 205d4ef5bb refactor: split admin.ts into sub-modules (P2-11)
Extracts 3833-line admin.ts into focused modules:
- src/routes/admin/ui.ts       — layout() and clampText() helpers
- src/routes/admin/helpers.ts  — KV list helpers (listAllFeeds, addFeedToList, etc.)
- src/routes/admin/feeds.ts    — feed CRUD routes (feedsRouter)
- src/routes/admin/emails.ts   — email view/delete routes (emailsRouter)

admin.ts now mounts the sub-routers and retains only auth middleware,
dashboard, login/logout, and the in-place feed API update route.
All 163 tests continue to pass.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 10:56:20 +02:00
Julien HerrandClaude Sonnet 4.6 a9501d6e44 feat: add structured JSON logger and worker utility (P1-4)
Introduces src/lib/logger.ts emitting JSON lines (level, message, data)
compatible with Cloudflare Logpush. Replaces all console.log/warn/error
calls in email-processor.ts, index.ts, and hub.ts with structured logger
calls. Extracts waitUntilSafe into src/utils/worker.ts to avoid duplicating
the executionCtx guard across routes.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 10:55:15 +02:00
Julien HerrandClaude Sonnet 4.6 0c0669c473 feat: extract constants module (P2-10)
Centralises magic numbers and string literals into src/config/constants.ts
(FEED_MAX_BYTES, MAX_FEED_ITEMS, MAX_METADATA_EMAILS, ADMIN_COOKIE_MAX_AGE,
FORWARD_EMAIL_IPS_CACHE_TTL_MS, DEFAULT_LEASE_SECONDS, MAX_LEASE_SECONDS,
FEEDS_LIST_KEY). Consumers updated in storage.ts and feed-fetcher.ts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 10:55:01 +02:00
Julien HerrandClaude Sonnet 4.6 582108a8e3 docs: move Durable Objects migration from TECH_DEBT to TODO
The race condition is an accepted limitation of KV; the DO migration
is a future feature rather than active tech debt to remediate.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 10:31:41 +02:00
Julien HerrandClaude Sonnet 4.6 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 <[email protected]>
2026-05-22 10:28:26 +02:00
Julien HerrandClaude Sonnet 4.6 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 <[email protected]>
2026-05-22 09:46:55 +02:00
Julien Herr f2981eec31 test: add inbound route tests covering IP auth, validation, and R2 upload 2026-05-22 07:39:48 +02:00
Julien Herr e874906291 docs: inline Durable Objects migration TODO in email-processor 2026-05-22 07:39:43 +02:00
Julien Herr 10404efffa fix: delete orphaned KV entries when trimming feed metadata 2026-05-22 07:39:37 +02:00
Julien Herr dae5db2524 feat: add GET /health endpoint 2026-05-22 07:39:24 +02:00
Julien HerrandClaude Sonnet 4.6 bde06dd3e4 fix(websub): add missing WebSub Link header to Atom feed
The RSS feed already advertised hub and self via the Link response
header, but the Atom feed was missing it entirely. Subscribers using
Atom had no way to discover the hub for real-time push notifications.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:51 +02:00
Julien HerrandClaude Sonnet 4.6 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 <[email protected]>
2026-05-21 23:46:51 +02:00
Julien HerrandClaude Sonnet 4.6 57e0cc5413 refactor(cors): replace manual CORS middleware with hono/cors
Fixes a bug where routes returning raw `new Response()` (RSS, Atom,
entries) were not receiving CORS headers — hono/cors applies headers
after next(), covering all response paths.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:51 +02:00
Julien Herr ece237c0af chore: ignore coverage/ directory 2026-05-21 23:46:51 +02:00
Julien HerrandClaude Sonnet 4.6 68151cbb5f fix(websub): require feed existence for subscriptions, remove atom hub header, simplify router mounting
- Add KV feed existence check in hub.ts to prevent SSRF via non-existent feeds (returns 404)
- Treat empty string hub.secret as absent (|| instead of ??)
- Remove misleading hub Link header from atom.ts (hub only supports RSS topics)
- Simplify double-layered hub router in index.ts (direct app.route instead of nested Hono)
- Update hub.test.ts to seed KV with feed config for tests requiring valid subscribe/unsubscribe

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:50 +02:00
Julien HerrandClaude Sonnet 4.6 0d00e003d4 test(websub): add ctx.waitUntil coverage for processEmail notification wiring
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:50 +02:00
Julien HerrandClaude Sonnet 4.6 d0764ddd8e feat(websub): wire real-time push notifications on email ingest
Pass ExecutionContext through the email processing chain so notifySubscribers
is called via ctx.waitUntil after a new email is stored.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:50 +02:00
Julien Herr 6d221a07dd feat(websub): add hub discovery Link headers to RSS and Atom feeds 2026-05-21 23:46:50 +02:00
Julien Herr aa41337c6b feat(websub): mount /hub route 2026-05-21 23:46:50 +02:00
Julien HerrandClaude Sonnet 4.6 09db52bb4d test(websub): add hub route tests
Add comprehensive tests for POST /hub validation (missing fields, unknown mode, non-HTTPS callback, invalid URL, wrong domain, secret > 200 bytes) and happy-path subscribe/unsubscribe (202). Also fix hub.ts to use a waitUntilSafe wrapper so executionCtx.waitUntil doesn't throw in Node test environments.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 23:46:50 +02:00
Julien Herr 4165774667 fix(websub): validate callback URL (HTTPS), fix domain regex, enforce secret length 2026-05-21 23:46:50 +02:00
Julien Herr ee4b9d8fdc feat(websub): add hub route (subscribe/unsubscribe) 2026-05-21 23:46:49 +02:00
Julien Herr e8f5af8b87 test(websub): add missing error-path tests for verify functions 2026-05-21 23:46:49 +02:00
Julien Herr c6785554d4 test(websub): add unit tests for WebSub utilities 2026-05-21 23:46:49 +02:00
Julien Herr 0b4ed0fbec fix(websub): fix signature header, add delivery logging, parallelize KV reads 2026-05-21 23:46:49 +02:00
Julien Herr 8810109e6f feat(websub): add WebSub utilities (HMAC, KV helpers, notify, verify) 2026-05-21 23:46:49 +02:00
Julien Herr ed6d2b4a0c feat(websub): add WebSubSubscription type 2026-05-21 23:46:49 +02:00
Julien HerrandClaude Sonnet 4.6 6fddbd40f2 feat: rename project to kill-the-news, add GitHub Pages landing page
- Rename all references from Email-to-RSS/email-to-rss to kill-the-news
  across README.md, AGENTS.md, package.json, wrangler-example.toml, setup.sh
- Add docs/index.html: dark-themed landing page for GitHub Pages
  covering features, how it works, quick start, and tech stack

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 20:45:25 +02:00
Julien Herr b0c31ac8ef Create CNAME 2026-05-21 20:32:22 +02:00
Julien HerrandClaude Sonnet 4.6 b26990a875 fix: address PR review comments
- Fix KV json overload to return Promise<unknown | null> (null on missing keys)
- Add shebang to Husky pre-commit hook
- Explicitly add eslint ^10.0.0 to devDependencies

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 12:09:26 +02:00
Julien HerrandClaude Sonnet 4.6 b24ee969d1 style: fix Prettier formatting on 11 files
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 11:35:37 +02:00
Julien HerrandClaude Sonnet 4.6 3aea41f862 feat: add ESLint, lint-staged, and update pre-commit hook + CI
- Add ESLint 9 flat config (eslint.config.mjs) with typescript-eslint
  recommended rules and eslint-config-prettier
- Add lint-staged to run eslint+prettier only on staged files
- Update pre-commit hook to use lint-staged instead of full prettier check
- Add `lint` and `format:check` scripts to package.json
- Add Lint step to CI workflow
- Fix resulting lint errors: unused vars (_ctx, _options, catch binding),
  any→unknown in type declarations, stale eslint-disable comments

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 09:49:20 +02:00
Julien HerrandClaude Sonnet 4.6 e93bbb8d3e feat: store email attachments in R2 and expose as RSS enclosures
Attachments from incoming emails are uploaded to an optional Cloudflare R2
bucket and exposed as <enclosure> elements in RSS and <link rel="enclosure">
in Atom feeds, served at /files/{id}/{filename} with immutable caching.

R2 is opt-in: if ATTACHMENT_BUCKET is not bound the feature is a no-op.
Attachments are cleaned up from R2 on email/feed deletion and during
size-based feed trimming. Adds MockR2 to the test setup.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 09:09:37 +02:00
Julien Herr 3e28246c61 docs: document FEED_MAX_SIZE_BYTES and proxy auth setup in README 2026-05-21 08:42:48 +02:00
Julien HerrandClaude Sonnet 4.6 caaa6a7ba6 feat: add external proxy auth support (Authelia/Authentik)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 08:39:10 +02:00
Julien HerrandClaude Sonnet 4.6 9eba4c34c6 feat: replace fixed 50-entry cap with size-based feed trimming
Emails are now trimmed from the oldest end when total serialised size
exceeds FEED_MAX_SIZE_BYTES (default 512 KB). Each EmailMetadata entry
stores its size so future trims are computed without re-reading KV.
Adds FEED_MAX_SIZE_BYTES, PROXY_TRUSTED_IPS and PROXY_AUTH_SECRET to Env.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 08:28:07 +02:00
Julien HerrandClaude Sonnet 4.6 984362f637 docs: add CLAUDE.md with project guidance for Claude Code
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 07:40:00 +02:00
Julien Herr 19b590a568 docs: mark Atom feed format as done in TODO 2026-05-21 07:36:14 +02:00
Julien Herr 1acc2f952c feat: mount /atom route in main app 2026-05-21 07:36:05 +02:00
Julien Herr b107803177 feat: add Atom 1.0 feed route at /atom/:feedId 2026-05-21 07:35:32 +02:00
Julien Herr 4d8d1fdc82 refactor: extract buildFeed helper, add generateAtomFeed 2026-05-21 07:34:29 +02:00
Julien HerrandClaude Sonnet 4.6 ad5bd9a79a docs: document Cloudflare Email Workers as primary ingestion method
Add setup instructions for Cloudflare Email Routing (Option A, recommended)
and reposition ForwardEmail as an optional alternative (Option B). Clarify
Cloudflare free plan includes Workers, KV, and Email Routing. Fix security
note scope and remove redundant/misleading content.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 00:36:44 +02:00
Julien HerrandClaude Sonnet 4.6 1c8b0ca194 ci: add CI workflow for format, typecheck, and test
Runs on push/PR to main. Also adds skipLibCheck to tsconfig to silence
pre-existing errors in vite/vitest declaration files that don't affect
the Worker build.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 00:07:25 +02:00
Julien HerrandClaude Sonnet 4.6 2e9cdd4364 test: add unit tests for EmailParser
Covers extractFeedId, decodeEncodedWords, and parseForwardEmailPayload
including edge cases: missing fields, structured vs text from, headerLines
vs raw headers string, RFC 2047 subject decoding.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 00:06:13 +02:00
Julien HerrandClaude Sonnet 4.6 41efee44ca refactor: replace custom escapeHtml with Hono's html template
Hono's `html` tagged template auto-escapes all interpolated values;
`raw()` is used for the email body which must render as HTML.
This removes the ad-hoc utility and aligns entries.ts with the
same pattern already used in admin.ts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-21 00:05:35 +02:00
Julien HerrandClaude Sonnet 4.6 5308544672 refactor: simplify quick-win code after review
- Make feedId required in generateRssFeed (removes dead /emails/ fallback)
- Hoist loop-invariant conditional and remove intermediate variable
- Extract normalizeAllowedSenders() so JSON and form paths share same logic
- Move escapeHtml to src/utils/html.ts for reuse by admin.ts
- Parallelize the two independent KV puts in feed creation

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:55:17 +02:00
Julien HerrandClaude Sonnet 4.6 e140b433d8 docs: mark quick wins as done in TODO.md
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:52:26 +02:00
Julien HerrandClaude Sonnet 4.6 fdedbe13c4 feat: accept JSON on POST /admin/feeds/create and return {feedId, email, feedUrl}
When Content-Type is application/json, parse the request body as JSON and
return a JSON response instead of redirecting. Useful for automation tools
(e.g. Terraform/OpenTofu provisioning).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:52:14 +02:00
Julien HerrandClaude Sonnet 4.6 298765527c feat: add HTML view for individual email entries at /entries/:feedId/:receivedAt
Serves each email as a standalone HTML page with a Content-Security-Policy
header, useful for reading emails outside a feed reader and for debugging.
Also updates RSS item links to point to this route.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:51:28 +02:00
Julien HerrandClaude Sonnet 4.6 54e7a1bfa0 feat: parse <author> from From header in RSS items
Parse the From header into name + email parts so the feed library
renders proper RFC 2822 format (email (Name)) in <author> elements.
Also passes feedId to the generator so item links can point to the
upcoming /entries/:feedId/:receivedAt route.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:50:54 +02:00
Julien HerrandClaude Sonnet 4.6 6a6614fb26 docs: add TODO.md with feature gaps vs kill-the-newsletter
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:44:44 +02:00
Julien HerrandClaude Sonnet 4.6 da6494cbd8 ci: pin action versions to SHA + add github-actions to Dependabot
Dependabot will now open PRs when new versions of actions/checkout and
actions/setup-node are released, keeping the pinned SHAs up to date.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-20 23:34:59 +02:00