- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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 entities.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>