Commit Graph

162 Commits

Author SHA1 Message Date
Huang Xin 4fa7f76bc1 feat(payment): observability for store subscription webhooks (#4704)
* feat(payment): observability for store subscription webhooks

Add monitoring for the App Store / Google Play webhooks so store-side
subscription changes are observable on Cloudflare, where stored Workers
Logs are head-sampled at 1% and would miss almost all low-volume webhook
events.

- Add iap/telemetry.ts: every webhook invocation emits a structured log
  line (streamed in full by `wrangler tail`) and a Cloudflare Analytics
  Engine data point (100% capture, independent of log sampling). Writes
  no-op off the Worker runtime, mirroring the getCloudflareContext guard
  in deepl/translate.ts.
- Instrument both webhook routes to record outcome (handled, skipped,
  rejected, error), notification type, status, reason, and latency on
  every return path.
- Add GET /api/cron/iap-reconcile: a CRON_SECRET-protected sweep that
  counts drift (rows still active while their store expiry has passed = a
  missed webhook) in both IAP tables and records a reconcile metric.
  Detection-only; never mutates state.
- Add the IAP_WEBHOOK_AE Analytics Engine binding to wrangler.toml.

New configuration: CRON_SECRET (reconcile auth) and an iap_webhooks
Analytics Engine dataset bound as IAP_WEBHOOK_AE. The reconcile route is
triggered on a schedule (a Cloudflare Cron Trigger worker that fetches
the URL, or any external scheduler) with an Authorization bearer header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(payment): move reconciliation to a dedicated cron Worker

Replace the public CRON_SECRET-protected /api/cron/iap-reconcile route
with a dedicated Cloudflare Cron Worker. A Cron Trigger invokes the
worker's scheduled() handler directly, so there is no public HTTP surface
and no shared request secret to manage - the strongest option on
Cloudflare (OpenNext's generated worker only exports `fetch`, so the main
worker cannot host a scheduled() handler).

- Add workers/iap-reconcile: a self-contained worker (own package.json,
  tsconfig, wrangler.toml) matching the existing workers/send-email
  convention, registered in pnpm-workspace.yaml. Hourly Cron Trigger;
  reads the IAP tables via the Supabase service role and records a drift
  metric to the shared iap_webhooks Analytics Engine dataset.
- Reconcile logic lives in workers/iap-reconcile/src/reconcile.ts and is
  unit-tested from the app suite.
- Remove the public route and its test; drop the now-unused
  recordIapReconcile from iap/telemetry.ts (webhook telemetry unchanged).

Configuration: set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY as secrets
on the worker and deploy it with `wrangler deploy` from its directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:02:02 +02:00
Huang Xin 353d381427 fix(deps): bump undici and dompurify overrides for security advisories (#4684)
Raise the pnpm-workspace transitive overrides to the patched versions:

- undici >=7.24.0 -> >=7.28.0 <8 (resolves to 7.28.0). Fixes 7 advisories:
  WebSocket DoS, SOCKS5 cross-origin routing, SOCKS5 TLS bypass,
  Set-Cookie header injection, shared-cache info disclosure, response
  queue poisoning, and SameSite downgrade (GHSA undici < 7.28.0).
  Bounded below 8 to stay on the patched 7.x line and avoid an
  unvetted major bump (an open-ended range pulled undici 8.5.0).
- dompurify >=3.4.9 -> >=3.4.11 (resolves to 3.4.11). Fixes permanent
  ALLOWED_ATTR pollution via setConfig() (<= 3.4.10).

Verified: pnpm test, pnpm lint, pnpm build-web all pass; lockfile sweep
confirms no vulnerable undici/dompurify versions remain.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:30:20 +02:00
Huang Xin 6c7c86f346 feat(applock): biometric unlock (fingerprint / Face ID) at startup on mobile (#4650)
* feat(applock): wire up biometric plugin + biometricUnlockEnabled setting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(applock): add guarded biometric service wrapper

* feat(applock): auto-prompt biometrics on the lock screen with PIN fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(applock): guard concurrent biometric prompts + tighten lock-screen tests

- Add biometricInFlightRef to prevent concurrent authenticateWithBiometrics calls
- Assert PIN input still rendered after biometric failure (test 2)
- Replace flaky waitFor negative assertion with a 50ms flush in test 3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(applock): mobile biometric toggle + default-on at PIN setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(i18n): add biometric app-lock strings

* fix(applock): seed biometricUnlockEnabled via app-lock store init; close test gaps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* build(applock): pin tauri-plugin-biometric in Cargo.lock

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:39:03 +02:00
Huang Xin f6fbbf59f2 chore(deps): bump transitive deps for security advisories (batch) (#4620)
Resolve a batch of transitive Dependabot alerts on the web lockfile via
pnpm-workspace overrides.

Bumped existing overrides:
- vite >=7.3.5 (#244 high, #245 med; GHSA path within 7.3.x) <- was 7.3.2
- dompurify >=3.4.9 (#249-#255; clears #252 <=3.4.6 by leaving the range)
- protobufjs >=7.6.3 <8 (#233, #247, #248); bounded <8 to stay on the
  patched 7.x line (a bare floor let pnpm jump to the 8.x major)
- ws >=8.21.0 (#241 high) <- was pinned 8.20.1

Added overrides:
- form-data >=4.0.6 (#246 high)
- js-yaml >=4.2.0 (#243 med)
- '@babel/core' >=7.29.6 (#242 low)
- '@opentelemetry/core' >=2.8.0 (#256 med)

Not auto-fixable here: #236 @ai-sdk/provider-utils (<=3.0.97, no recorded
fix; the installed 3.0.25 is pinned via a local patchedDependencies patch).

Verified: pnpm test (5685 passed), pnpm lint, pnpm build-web (exit 0).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:19:36 +02:00
Huang Xin d6e59cedd7 chore(deps): bump esbuild to 0.28.1 and vitest to 4.1.x for security advisories (#4618)
Resolve transitive Dependabot alerts on the web lockfile.

esbuild >= 0.28.1 (GHSA-gv7w-rqvm-qjhr #239, GHSA-g7r4-m6w7-qqqr #238):
Deno-module RCE via NPM_CONFIG_REGISTRY and Windows dev-server arbitrary
file read. Forced via a pnpm-workspace override (esbuild is a regular dep
of vite, so the override applies cleanly); bounded to <0.29 to stay on the
verified line. vite 7.3.3 declares ^0.27.0, but the 0.28 JS API is
unchanged for vite's usage -- verified by the full test run and web build.

@vitest/browser >= 4.1.8 (GHSA-g8mr-85jm-7xhm #240): Browser Mode CDP
bridge bypasses allowWrite/allowExec, enabling config overwrite -> RCE.
Bumped the vitest devDep family (vitest, @vitest/browser-*, coverage-v8)
from ^4.0.18 to ^4.1.8; resolves to 4.1.9.

Verified: pnpm test (5685 passed), pnpm lint, pnpm build-web (exit 0).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:01:17 +02:00
Huang Xin 607e646bc6 chore(deps): bump shell-quote to 1.8.4 and qs to 6.15.2 for security (#4523)
Raise the pnpm overrides so the patched transitive versions are pulled:

- shell-quote >=1.8.4 fixes GHSA-w7jw-789q-3m8p / CVE-2026-9277 (critical):
  quote() failed to escape newlines in object .op values, allowing shell
  command injection. Pulled in via cpx2.
- qs >=6.15.2 fixes GHSA-q8mj-m7cp-5q26 / CVE-2026-8723 (medium):
  qs.stringify DoS on null/undefined entries in comma-format arrays with
  encodeValuesOnly. The prior >=6.14.2 pin still allowed vulnerable 6.15.1.
  Pulled in via express, body-parser, googleapis-common.

Resolves Dependabot alerts:
- https://github.com/readest/readest/security/dependabot/237 (shell-quote)
- https://github.com/readest/readest/security/dependabot/235 (qs)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:19:25 +02:00
loveheaven 726f53a64b fix(reader): use Tauri clipboard plugin for copy on Android (#4409)
navigator.clipboard.writeText is unreliable inside the Tauri Android
WebView, so tapping Copy in the Reader's selection popup silently
no-ops. Route the write through @tauri-apps/plugin-clipboard-manager
on Tauri targets (Android, iOS, macOS, Windows, Linux), with a
graceful navigator.clipboard / execCommand fallback for the web
build and older WebViews.

- Add tauri-plugin-clipboard-manager (Rust + JS)
- Register the plugin in lib.rs
- Grant clipboard-manager:allow-write-text / allow-read-text
- New utils/clipboard.ts wrapper with platform-aware fallback chain
- Annotator handleCopy and handleConfirmExport use the wrapper
2026-06-02 16:02:52 +02:00
Huang Xin 9b4db44490 fix(pdf): ship jbig2.wasm so scanned PDFs render in packaged builds (#4382)
pdfjs-dist 5.7.x (bumped 5.4.530 -> 5.7.284 in #4143) moved the JBIG2
image decoder -- the codec used by virtually every black-and-white
*scanned* PDF -- from pure JS into a WebAssembly module the worker
fetches at runtime from `wasmUrl` (/vendor/pdfjs/). The `copy-pdfjs-wasm`
script only copied an explicit allow-list ({openjpeg.wasm,qcms_bg.wasm})
and silently dropped jbig2.wasm. cpx does not error on an empty glob, so
the missing decoder went unnoticed: scanned PDFs rendered blank (pages
still turned) in CI builds, while local `tauri build` reused a stale
pre-bump public/vendor copy and worked. Regression between 0.11.1 and
0.11.2.

Copy the whole wasm/ dir (mirrors the {cmaps,standard_fonts}/* fonts
pattern) so future decoders moving to wasm can't be dropped again, and
ship the *_nowasm_fallback.js files for graceful degradation.

Adds a regression test asserting every .wasm the bundled pdf.js
references is covered by copy-pdfjs-wasm.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:52:10 +02:00
Huang Xin 4a5674ef4f chore: bump turso to version 0.6.1 (#4312) 2026-05-26 18:25:28 +02:00
Huang Xin e0ce6c8c22 feat(reedy): Appendix A · Phase 4 — custom thread UI on AgentRuntime (#4308) 2026-05-26 14:55:20 +08:00
loveheaven 98049282eb feat(ai): add OpenRouter provider and unify provider HTTP transport (#4289)
* OpenRouter: new OpenAI-compatible provider with chat + embedding model
  routing, health check via /models, and a fetchOpenRouterModels() helper
  for the settings UI. API key, base URL and model fields are persisted in
  AISettings, surfaced in AIPanel, indexed by commandRegistry, and added
  to backupService's credential allow-list so the key round-trips through
  encrypted backups.

* utils/httpFetch: introduce getAIFetch() as the single decision point for
  outbound AI traffic. In Tauri it returns @tauri-apps/plugin-http's fetch
  (Rust/reqwest transport, no renderer CORS preflight, no Android cleartext
  block); on the web build it falls back to window.fetch. OllamaProvider
  is migrated end-to-end — both ai-sdk-ollama streaming and the /api/tags
  health probe — and the new OpenRouterProvider uses the same path, so any
  future provider only has to call getAIFetch().

* Tests: unit tests for OpenRouter provider behavior (model selection,
  availability, health check) and a backup-settings round-trip test
  ensuring openrouterApiKey is treated as a credential field.
2026-05-25 18:32:07 +02:00
Huang Xin 8a19c686cb ci: address code scanning scorecard alerts (#4275) 2026-05-22 20:20:10 +02:00
Huang Xin f6dfd09d82 feat(send): browser extension that clips pages into Readest as EPUBs (#4266)
Replaces the URL-only placeholder extension with a full MV3 page-clipper
that builds a self-contained EPUB on the user's machine and uploads it
to the inbox.

- Captures the rendered DOM in a content script, then runs Readability,
  asset bundling, and EPUB build through the shared
  `convertToEpub({kind: 'page'})` pipeline inside a Chrome offscreen
  document (the SW lacks DOMParser).
- Uploads the resulting EPUB directly from the offscreen page to the
  new `POST /api/send/inbox/file` endpoint — keeps the bytes in one
  realm because `runtime.sendMessage` JSON-serialises ArrayBuffer to
  `{}` between extension contexts.
- Adds a long-lived Port + ping handshake between SW, offscreen, and
  the on-demand capture content script so neither idle-eviction nor
  load-order races can hang the popup.
- Localised popup, badge feedback, key-as-content i18n (`_('English source')`)
  with an extract script that seeds locale stubs from i18n-langs.json
  and writes a static-imports map for the runtime. All 33 locales
  fully translated.
- Server: `pages/api/send/inbox/file.ts` accepts a raw EPUB body
  (Content-Type: application/epub+zip), enforces the inbox pending cap,
  stores to the existing send-inbox R2 bucket as `kind='file'`.
  `assetBundler` now sets `credentials: 'include'` in the non-Tauri
  branch so the extension SW carries paywalled-CDN cookies.
- 47 vitest cases for the extension shell (upload, badge, auth, lazy,
  popup state machine, auth-bridge token sync) + 8 cases for the new
  server endpoint. CI's `test_web_app` invokes both via the extended
  `test:pr:web` plus a `build-browser-ext` step that catches webpack
  alias / Tauri-stub regressions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:45:29 +02:00
Huang Xin 0b18de0581 feat(send): Send to Readest — multi-channel capture into your library (#4230)
* 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>
2026-05-19 20:06:52 +02:00
Huang Xin d25c41ee89 chore(security): address code scanning findings (#4224) 2026-05-19 09:01:07 +02:00
Huang Xin fe41c42ec5 chore: switch code formatter from Prettier to Biome (#4223)
Replace Prettier with Biome for formatting JS/TS/JSX/CSS/JSON. The CI
format check drops from ~23s to ~0.4s.

- Unify config into a single root biome.json (formatter + linter); the
  former apps/readest-app/biome.json was linter-only
- Mirror the old .prettierrc.json style: 100 line width, 2-space indent,
  LF, single quotes, trailing commas
- Enable the CSS tailwindDirectives parser for @apply in globals.css
- Convert // prettier-ignore comments to // biome-ignore format:
- Root scripts and lint-staged now run biome; apps/readest-app lint runs
  `biome lint` (lint-only) so formatting stays a separate CI step
- Drop prettier + prettier-plugin-tailwindcss dependencies

Markdown/YAML are no longer format-checked (Biome does not format them)
and Tailwind class sorting is no longer enforced.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:13:36 +02:00
Huang Xin 28a7785e5d test(e2e): add a Playwright web e2e lane (reading & annotation flows) (#4214)
* feat(e2e): add Playwright web e2e lane

Adds a web-layer end-to-end suite that drives the Next.js web build
(`pnpm dev-web`) in a real browser, complementing the existing
WebdriverIO suite that drives the Tauri shell.

- playwright.config.ts: single Chromium project, auto-starts dev-web
- e2e/pages: BasePage/LibraryPage/ReaderPage page objects
- e2e/fixtures/base.ts: suppresses demo-book auto-import for a
  deterministic empty library
- e2e/tests: library shell + search, book import, reader open +
  pagination smoke specs
- e2e/fixtures/books: synthetic sample book for import tests
- scripts: test:e2e:web, test:e2e:web:ui, test:e2e:web:report

Tests run unauthenticated against isolated browser contexts;
authenticated/sync flows are out of scope until a test account is
provisioned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(e2e): cover reading and annotation flows

Expands the Playwright web e2e lane beyond library/import smoke tests to
exercise the major reading and annotation features against the real
sample-alice.epub fixture (src/__tests__/fixtures/data/).

Reading (reading.spec.ts): open + page turn, TOC chapter navigation,
in-book search, font-size change via the settings dialog, bookmark
toggle.

Annotation (annotation.spec.ts): selection popup, create highlight,
change highlight color, add a note, delete an annotation.

- ReaderPage POM gains sidebar/TOC, search, settings, bookmark and
  annotation actions; text selection is driven inside the section
  iframe (synthetic drags do not produce a selection through nested
  paginated foliate iframes)
- openBook fixture imports and opens a book so specs skip boilerplate
- books.ts centralises fixture book paths
- replaces the old reader.spec.ts smoke

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(e2e): add headed run script and always write HTML report

- test:e2e:web:headed runs the suite in a visible browser, one test at
  a time, with traces captured
- the HTML reporter now runs for local runs too, so every run writes
  playwright-report/ for test:e2e:web:report to open

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(e2e): fix headed-run flakes in reading and annotation specs

The headed run (slower rendering) surfaced two races that the headless
run happened to pass:

- TOC navigation read reading progress before the section's async
  progress update landed — now polls with expect.poll.
- visibleSectionFrame required a paragraph fully inside the viewport,
  which intermittently matched nothing — now accepts any paragraph
  intersecting the viewport and tolerates frames detaching mid-navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: run the Playwright web e2e suite in test_web_app

Adds `pnpm test:e2e:web` to the test_web_app job, after the unit/browser
tests. The job already installs the Chromium browser, and `.env.web` is
committed so the auto-started `pnpm dev-web` server has its config. On
failure the HTML report is uploaded as an artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(e2e): exclude e2e specs from the vitest run

vitest's default glob matches `*.spec.ts`, so it picked up the new
Playwright `e2e/tests/*.spec.ts` files and crashed. Exclude `e2e/`
from vitest — those specs run via `pnpm test:e2e:web`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(e2e): run the web e2e suite against a production build

`next dev` renders a full-screen error overlay when the app emits its
`next-view-transitions` "Transition was aborted" unhandled rejection,
and the overlay intercepts pointer events — making the suite flaky on
CI. CI now builds the web app (`pnpm build-web`) and the Playwright
webServer serves it via `pnpm start-web`; local runs still use
`pnpm dev-web`. Verified: 14/14 pass against the production build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(e2e): run the web e2e suite in the build_web_app job

build_web_app already runs `pnpm build-web`, so the e2e suite belongs
there — it reuses that build (the CI Playwright webServer serves it via
`pnpm start-web`) instead of building a second time in test_web_app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(e2e): run the web e2e suite with 4 workers

Specs are isolated (a fresh browser context per test), so they are
safe to parallelize. `test:e2e:web:headed` keeps --workers=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:22:17 +02:00
Huang Xin e8df651d5a chore(deps): bump Next.js to version 16.2.6 (#4143) 2026-05-13 10:57:06 +02:00
Huang Xin aea3fda086 chore: bump turso to the latest version (#4086) 2026-05-07 20:11:47 +02:00
Huang Xin 5a0a70a30a feat(reader): custom dictionaries (StarDict + MDict) (#4012)
* feat(reader): custom dictionaries (StarDict + MDict)

Adds a pluggable dictionary provider system. Built-in Wiktionary +
Wikipedia (extracted from the legacy single-popup model into a tabbed
shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict
(.mdx/.mdd) bundles.

Settings → Language → Dictionaries: import / enable / drag-reorder /
delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with
pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup
history, scroll-aware back button, last-active tab persists. Tabs grow
to natural width up to a cap, truncate with ellipsis when crowded; phantom
bold layer prevents layout shift on focus.

StarDict reader is self-contained (replaces unused foliate-js/dict.js),
with lazy random-access binary search on .idx + .syn (~420 KB Int32Array
of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk
decompression via fflate streaming Inflate (cmudict/eng-nld both
chunked), and an optional .idx.offsets sidecar generated at import to
skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and
2 MB IO to ~1.7 MB heap and ~500 KB IO.

MDict uses the readest/js-mdict fork (added as a submodule, consumed via
tsconfig paths so deps stay out of readest's pnpm-lock) which adds a
browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer()
— slices are lazy when the Blob is Readest's NativeFile / RemoteFile.
encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based
mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as
unsupported.

Wikipedia annotation tool removed (Wikipedia is now a tab inside the
unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale
annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings
load. iOS-friendly external links: skip target="_blank" on Tauri to
avoid the WebView's "open externally" path triggering the shell scope
error; the popup's container click handler routes through openUrl.

i18n: 939 strings translated across 31 locales (30 base keys + CLDR
plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es).

Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld
(StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2).
3396 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stardict): resolve fflate from js-mdict source for vitest + Next

js-mdict is consumed as TypeScript source via tsconfig paths from
packages/js-mdict/src/. Its sources `import 'fflate'` directly, but
fflate is only installed under apps/readest-app/node_modules — so
vite's import-analysis (and Next/Turbopack's resolver) can't find
fflate when it walks up from the redirected js-mdict source location.
CI's fresh checkout exposes this; locally a leftover
packages/js-mdict/node_modules/fflate from the old workspace setup
masked it.

Pin fflate resolution to apps/readest-app/node_modules/fflate in:
- vitest.config.mts (Vite alias)
- next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack
  rejects absolute paths so use a project-relative form)
- tsconfig.json (paths entry so tsgo / Biome see it)

Verified by deleting packages/js-mdict/node_modules locally and
re-running pnpm test (3396 pass), pnpm lint (clean), and both
pnpm build-web and a tauri-platform Next build (clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:16:36 +02:00
Huang Xin 920627ae59 feat(rsvp): use jieba tokenizer to segment words for Chinese books (#3985) 2026-04-28 18:19:05 +02:00
Huang Xin fa61f40262 chore: bump opennext and wrangler to the latest versions (#3971) 2026-04-27 10:56:59 +02:00
fabrisnick b251e537d3 chore(deps): update stripe (#3941)
* chore(deps): update stripe

* fix: filter and sort not affecting purchases

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor: rename restoredPurchases to restoredSubscriptions

The filter excludes one-time purchases, so the array contains only
subscriptions. Naming reflects that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:24:00 +02:00
Huang Xin be8b946683 chore: update Dependabot dependencies (#3938)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 12:27:39 +02:00
Huang Xin cc780712b9 fix(deps): add pnpm override for qs >=6.14.2 (Dependabot #71) (#3841)
Fixes DoS vulnerability from arrayLimit bypass in comma parsing.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 06:58:31 +02:00
Huang Xin 95ff526140 fix(deps): bump dependencies to resolve 13 Dependabot security alerts (#3840)
Update next (16.2.3), vite (7.3.2+), react/react-dom (19.2.5),
@vitejs/plugin-rsc (0.5.23), react-server-dom-webpack (19.2.5),
and add overrides for lodash (4.18.0), lodash-es (4.18.0),
basic-ftp (5.2.2) to fix high/medium severity vulnerabilities
including DoS, code injection, prototype pollution, CRLF injection,
arbitrary file read, and path traversal.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 06:44:30 +02:00
Huang Xin 3df75a67f9 feat(tts): support edge tts on cloudflare worker (#3819) 2026-04-10 15:32:06 +02:00
Huang Xin 7e62516b5d chore(deps): bump Next.js to version 16.2.2 (#3757) 2026-04-05 15:59:10 +02:00
Huang Xin 0e516f6e56 chore(test): add unit tests and enforce dash-case naming for test files (#3715)
Add ~290 new unit tests covering utils (lru, diff, css, a11y, walk,
usage, txt-worker), services (EdgeTTSClient, transformers), and
suppress noisy console output across all test files. Rename 27
camelCase test files to dash-case for consistency.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:14:00 +02:00
Huang Xin 956c71cd7b chore: migrate from ESLint to Biome for linting (#3694)
Replace ESLint with Biome for ~14x faster linting (~360ms vs ~5s).

- Add biome.json with rules matching ESLint parity (Next.js, a11y,
  TypeScript, unused vars/imports)
- Remove eslint, @typescript-eslint/*, eslint-config-next,
  eslint-plugin-jsx-a11y, @eslint/* from deps
- Remove eslint.config.mjs
- Update lint script to: tsgo --noEmit && biome check .
- Fix 11 real code issues caught by Biome (banned types, explicit any,
  unsafe finally, unreachable code, shadowed names)
- Disable Biome formatter (Prettier stays for Tailwind class sorting)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:25:04 +02:00
Huang Xin b4207bd742 chore(deps): bump vulnerable dependencies to address Dependabot alerts (#3693) 2026-03-30 15:59:03 +02:00
Huang Xin 97555a7e88 chore: bump next.js, opennextjs and wrangler to the latest versions (#3642) 2026-03-26 15:57:19 +01:00
Huang Xin e8f70b896e chore: bump next.js from 16.1.6 to 16.1.7 (#3569) 2026-03-19 18:11:26 +01:00
Huang Xin 25352efece chore: bump dependencies for Dependabot alerts (#3533) 2026-03-14 06:57:43 +01:00
Huang Xin d06e1849cc feat(rsvp): add persistent context, display settings, and layout improvements, closes #3333 (#3528)
* feat(rsvp): add persistent context, display settings, and layout improvements, closes #3333

Add always-visible collapsible context panel, font size adjustment,
ORP color selection, and stable context window that only rebuilds on
scroll. Refactor speed controls into playback row with collapsible
settings behind a gear icon. Translate new i18n keys across 29 locales.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selfhost): update docker db schema to match FileRecords (#3527)

In #2636, FileRecord was defined to have updated_at  field
which is used when it is accessed from the database. But the
local dev setup is missing this field.

This diff adds an updated_at column to match the expectation

* chore(ci): cache system dependency and rustc outputs in ci

* chore(ci): lint script changed from tsc to tsgo

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Azeem Bande-Ali <me@azeemba.com>
2026-03-13 06:57:47 +01:00
Huang Xin 8850e6c00f feat(pdf): support TTS and annotation on PDFs, closes #2149 & #3462 (#3493)
* chore: bump jsdom to the latest version

* feat(pdf): support TTS and annotation on PDFs, closes #2149 & closes #3462
2026-03-09 10:28:19 +01:00
Huang Xin dac4f2e8ec chore: experimental vinext build (#3486) 2026-03-06 18:04:28 +01:00
Huang Xin 13588b4a65 chore(testing): add Tauri integration tests and E2E test infrastructure (#3483)
Set up WebDriver-based testing for the Tauri app with two tiers:
- Vitest browser-mode tests (*.tauri.test.ts) running inside the Tauri WebView
  for plugin IPC testing (libsql, smoke tests)
- WDIO E2E tests (*.e2e.ts) for UI-level interaction testing

Key changes:
- Add webdriver Cargo feature gating tauri-plugin-webdriver
- Add runtime capability for remote URLs (webdriver builds only)
- Add vitest.tauri.config.mts and wdio.conf.ts connecting to embedded
  WebDriver server on port 4445
- Add shared tauri-invoke helper for IPC from Vitest iframe context
- Add testing documentation in docs/testing.md
2026-03-05 19:14:01 +01:00
Huang Xin 1e9bd1d821 chore(database): unit testing and feature detect for fts and vector search (#3476) 2026-03-05 07:41:16 +01:00
Huang Xin 02cc0a9ebb chore: bump turso to 0.5.0 (#3475) 2026-03-04 19:54:58 +01:00
Huang Xin 5273ef75dc feat(database): add database service abstraction with libsql/turso backend (#3472) 2026-03-05 02:38:23 +08:00
scinac 38552a0c2e feat(reader): adding current Time and Battery to Footer (#3306) (#3402)
* added current time to desktop bar

* added time prototype to footer, needs code cleanup and settings toggle

* fixed settings toggle, added translations and code cleanup

* added battery support and moved Statusbar to own Component

* #3306 added 24 hour clock support

* refactored code styling and getting rid of any type in battery hook

* Add battery info for Tauri Apps

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-03-03 17:10:13 +01:00
Huang Xin 0afff573a1 chore: bump dependencies to resolve Dependabot alerts (#3421) 2026-02-28 18:31:33 +01:00
Huang Xin 4465e6986e chore: add husky pre-commit and pre-push hooks (#3372) 2026-02-25 06:39:11 +01:00
Huang Xin 128b238bcb feat: add directional view transitions with scroll preservation in library view, closes #3357 (#3362) 2026-02-24 14:56:45 +01:00
Huang Xin 9834bd57cf feat(toc): add page number for nested TOC items, closes #2953 (#3174) 2026-02-05 08:19:18 +01:00
boludo00 bbbd378f9b feat: rsvp speed reading (#3121)
* feat: add RSVP speed reading feature

Implements Rapid Serial Visual Presentation (RSVP) speed reading mode:

- Display words one at a time with ORP (Optimal Recognition Point) highlighting
- Adjustable WPM speed (100-1000) with keyboard/button controls
- Punctuation pause settings (25-200ms)
- Progress tracking with chapter navigation
- Context panel showing surrounding text when paused
- Keyboard shortcuts (Space, Escape, arrows) and touch gestures
- Chapter selector for quick navigation
- Respects current theme colors
- Persists settings (WPM, pause duration, position) per book

New files:
- services/rsvp/RSVPController.ts - Main controller with playback logic
- services/rsvp/types.ts - TypeScript interfaces
- components/rsvp/RSVPOverlay.tsx - Full-screen RSVP reader UI
- components/rsvp/RSVPControl.tsx - Integration component
- components/rsvp/RSVPStartDialog.tsx - Start position selection

Closes #3111

* fix(rsvp): use portal to fix overlay stacking context issue

- Render RSVP overlay and start dialog via React Portal to document.body
- This ensures the overlay appears above all other content regardless of
  parent component CSS transforms or stacking contexts
- Add fallback colors for theme values to ensure solid background

* fix(rsvp): improve UX with progress sync, sentence highlight, and better dialogs

- Fix start dialog transparency by using solid opaque background with proper
  fallback colors for both light and dark modes
- Increase context words from 50 to 100 words before/after current word
- Add progress sync on RSVP exit - navigates reader to the last word position
- Add temporary bright green sentence underline on exit (5 second duration)
  to easily locate where reading left off
- Helper function to expand word range to full sentence boundaries

* fix(rsvp): store full BookNote for proper annotation removal

The addAnnotation API requires the full BookNote object (including CFI)
when removing annotations, not just the ID. Changed tempHighlightIdRef
to tempHighlightRef to store the complete BookNote object.

* test(rsvp): add Playwright e2e tests for RSVP feature

- Set up Playwright test infrastructure with config
- Add comprehensive e2e tests for RSVP speed reading:
  - Opening RSVP from View menu
  - Start dialog options
  - Play/pause toggle
  - Speed adjustment
  - Skip navigation
  - Keyboard shortcuts
  - Progress bar
  - Chapter navigation
  - Accessibility tests
- Add test data attributes and ARIA labels to RSVPOverlay
- Add test scripts to package.json

* fix(rsvp): clarify start dialog option labels

Update the RSVP start dialog to use clearer language:
- "From Beginning" → "From Chapter Start" (since it starts from chapter beginning)
- "From Current Position" → "From Current Page" (starts from visible page)

* fix(rsvp): use correct theme colors from themeCode

The RSVP components were using incorrect palette key names (camelCase
like `base100` instead of hyphenated like `base-100`), causing the
fallback colors to always be used instead of the reader's actual theme.

Fix by using themeCode.bg, themeCode.fg, and themeCode.primary directly,
which are already resolved from the palette with correct keys.

* fix(rsvp): use theme accent color for sentence underline and persist until page change

- Change underline color from hardcoded green (#22c55e) to theme accent
  color (themeCode.primary), matching the ORP focal point highlight
- Remove 5-second timeout that auto-removed the underline
- Add cleanup on page navigation, new RSVP session start, and unmount
- Add removeRsvpHighlight helper function for consistent cleanup

* fix(rsvp): transition to next chapter when reaching end of section

When RSVP reached the end of a chapter, it would restart the current
chapter instead of moving to the next one. This happened because RSVP
extracts ALL words from the current section via renderer.getContents(),
so when words run out, the entire chapter is done.

- Always use view.renderer.nextSection() when RSVP exhausts words
- This moves to the next chapter instead of staying in the current one

* refactor: remove test files, unsure of use case for now

* chore: remove Playwright e2e test scripts from package.json

- Deleted e2e test scripts related to Playwright from package.json as they are no longer needed.
- Removed Playwright as a dev dependency to streamline the project.

* fix(rsvp): ensure CFI retrieval occurs before navigation

- Updated comments to clarify the necessity of obtaining CFI for both the navigation and sentence highlight before invoking the goTo() method, as it may re-render the document and invalidate Range objects.
- Introduced a new variable for sentence text to enhance readability and maintainability of the code.

* chore: sync pnpm-lock.yaml after removing @playwright/test

* style: format RSVP components

* fix: lint errors and timezone-aware date formatting

* i18n: support CJK text and add translations

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-02-01 18:22:24 +01:00
Huang Xin 160efcd37b chore: bump next.js to the latest version (#3106) 2026-01-29 12:29:17 +01:00
Huang Xin d62ad60ce8 chore: bump opennextjs and wrangler to the latest versions (#3092) 2026-01-27 14:57:28 +01:00
Mohammed Efaz a52d9e3a2b feat: add fuzzy search for searching across all settings (#3085)
* feat: enable linking to settings items

* feat: integrate command palette with global state and styles

* feat: add command pallete ui and fuzzy search

* feat: add command palette with global state and styles

* feat: add command registry and search dependencies

* fix: open command palette with shortcuts in reader page

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-26 18:32:04 +01:00