Commit Graph

173 Commits

Author SHA1 Message Date
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 22:38:29 +02:00
Julien Herr c64ceb4ee7 feat(site): add GitHub Sponsors links (nav, sponsor section, footer)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:56:56 +02:00
Julien Herr 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 <noreply@anthropic.com>
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 Herr 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 <noreply@anthropic.com>
2026-05-22 21:12:22 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 21:12:22 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:43:06 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:41:21 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:28:13 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:21:49 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:21:42 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 18:21:34 +02:00
Julien Herr 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 <noreply@anthropic.com>
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 Herr 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 <noreply@anthropic.com>
2026-05-22 15:53:22 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 15:44:08 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 15:43:49 +02:00
Julien Herr d64f703820 fix(ui): replace children cast to any with hono/jsx Child type
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-22 15:35:10 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:53:33 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:50:54 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:50:47 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:50:41 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:50:31 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:17:46 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:17:38 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:17:31 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:17:24 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 13:17:16 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 12:01:42 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 10:56:55 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 10:56:20 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 10:55:15 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 10:55:01 +02:00
Julien Herr 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 <noreply@anthropic.com>
2026-05-22 10:31:41 +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 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 Herr 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 <noreply@anthropic.com>
2026-05-21 23:46:51 +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