* perf(send): dynamic-import the conversion fallback so /library stays lean
conversionWorker.ts value-imported convertToEpub for the no-Worker
fallback path. That pulled mammoth, @mozilla/readability, DOMPurify and
@zip.js/zip.js into the main bundle — eagerly loaded on /library via
useInboxDrainer's static import of conversionWorker.
Switch the fallback to `await import('./convertToEpub')`. The worker
entry still value-imports convertToEpub for its own chunk; the
main-thread fallback only loads the heavy deps when Workers are actually
unavailable or fail.
Measured on the production web build:
- before: /library eagerly loads the 634KB conversion chunk
- after: the 634KB chunk + its two ~627KB duplicates are all lazy
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(send): never clobber the library when /send writes before it has loaded
The /send page (and the inbox drainer when it races the library page's
load) called `useLibraryStore.updateBooks(envConfig, [book])` while the
store still held the empty initial library — `libraryLoaded: false`. The
merge ran against `[]`, so `saveLibraryBooks` persisted just the new
book as the *entire* library and sync pushed the clobbered copy to every
device.
Two-layer fix:
1. Harden `updateBooks`: if `libraryLoaded` is false, load the real
library from disk first, then merge — `updateBooks` is now self-
protecting against any future caller that forgets the load step.
2. Gate `useInboxDrainer` on `libraryLoaded`. The hook now subscribes to
the flag and starts draining the moment the library finishes loading,
instead of running the first pass against an empty in-memory copy.
Adds a regression test that fails without the store change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(send): Send to Readest — multi-channel capture into your library
A Send-to-Kindle equivalent: email, web-upload, share, or one-click
capture books and articles into the cloud library; they sync to every
device.
Architecture (client-side processing): out-of-app channels drop a raw
payload into a per-user send_inbox; Readest clients drain it through one
shared ingestService.ingestFile(). The server never parses or converts.
- ingestService.ingestFile() — channel-agnostic import orchestration
extracted from library/page.tsx (DI-based, forceUpload support).
- send_addresses / send_allowed_senders / send_inbox tables + RLS + 4
SECURITY DEFINER claim/lease RPCs (migration 012_send_to_readest.sql).
- Conversion subsystem (DOCX/RTF/HTML/article/TXT -> EPUB) in a Web Worker.
- send-email Cloudflare Email Worker; inbox-drainer controller +
useInboxDrainer hook; /api/send/* routes.
- Send to Readest settings panel: inbound address, approved-sender
allowlist, recent activity, per-device drain toggle.
- /send web page (file drop + article URL) + SSRF-guarded fetch-url proxy.
- OS-shared files routed through ingestFile; Manifest V3 browser extension.
Security: inbox state changes only via SECURITY DEFINER RPCs (clients get
SELECT-only on send_inbox); approved-sender allowlist gates email;
SSRF guard on the one server-side URL fetch; inbox payload signed URLs
authorize against send_inbox.user_id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: run format:check in the pre-push hook
Biome format checking is fast (~0.4s), so gate pushes on it too — catches
mis-formatted files that bypassed the staged-only pre-commit hook before
they reach CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(send): address CodeQL security findings
- ReDoS (senders.ts): the email regex had ambiguous quantifiers around
the literal dot. Rewrote it linear-time (domain labels exclude '.')
and cap the input at 254 chars.
- XSS (convertToEpub.ts): run untrusted HTML through DOMPurify
(sanitizeForParsing — keeps document structure) before DOMParser, so
title extraction and Readability never parse executable markup.
- SSRF (fetch-url.ts): harden the host guard — block bare single-label
hostnames, IPv4-mapped IPv6, CGNAT/benchmark/multicast ranges, and the
unspecified address. DNS rebinding stays a documented residual risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>