Commit Graph

44 Commits

Author SHA1 Message Date
Huang Xin 883dae36aa fix(opds): auth negotiation and auto-download fixes for self-hosted catalogs (#5002)
* fix(opds): allow LAN catalogs through the proxy in development

The SSRF host blocklist added in #4638 unconditionally rejected private
addresses, so the dev proxy returned 400 for LAN OPDS catalogs even though
next dev runs on the developer's own machine where reaching the LAN is the
normal use case. Skip the blocklist when NODE_ENV is development, matching
the existing CatalogManager gate that only forbids LAN URLs in production.
Production and test behavior are unchanged.

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

* fix(opds): negotiate Digest auth when the server rejects preemptive Basic with 400

Calibre's content server in digest mode, or auto mode over http, answers a
Basic Authorization header with 400 Unsupported authentication method
instead of a 401 challenge. The preemptive Basic header introduced in #4206
therefore dead-ended the request, since the auth retry only fired on 401 or
403, and digest catalogs failed with Failed to load OPDS feed: 400 Bad
Request on every platform. When a request that carried preemptive Basic
comes back 400, re-issue it once without credentials to surface the
WWW-Authenticate challenge and let the existing negotiation pick the scheme
the server actually wants. Verified end to end against a live Calibre
server on web and Android.

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

* fix(opds): skip SSL verification in auto-download like the manual download path

The native download_file command validates TLS with rustls, which ignores
the OS trust store, so downloads from self-signed or private-CA OPDS
servers fail in the TLS handshake before any request reaches the server.
The manual download path has passed skipSslVerification since #2900 as the
workaround for #2871, but the auto-download path never did, so subscribed
shelves failed to sync while feed browsing and manual downloads of the same
books worked. Pass the same flag in downloadAndImport.

Closes #4988

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-07-07 19:47:26 +02:00
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 359fdddcf4 feat(payment): handle App Store and Google Play subscription webhooks (#4701)
Add server-push endpoints so store-side subscription changes (cancel,
refund, expire, renew, grace period) are reflected in the database, not
only the in-app verification flow. Previously only Stripe had a webhook.

- POST /api/apple/notifications: verify and decode App Store Server
  Notifications V2, resolve the user by original_transaction_id, map the
  notification type to a status, and update the subscription and plan. A
  single endpoint serves Sandbox and Production. Refunded one-time
  purchases are marked refunded and storage is recomputed.
- POST /api/google/notifications: verify the Pub/Sub shared-secret token,
  decode the RTDN, resolve the user by purchase_token, re-verify against
  the Play Developer API (overriding the status for terminal events such
  as REVOKED/EXPIRED and grace period), and handle voided purchases.
- Add an isEntitledStatus helper and reuse the existing
  createOrUpdateSubscription and plan-update logic shared with Stripe.

New configuration: GOOGLE_RTDN_VERIFICATION_TOKEN (shared secret in the
Pub/Sub push URL) and the optional GOOGLE_IAP_PACKAGE_NAME; Apple reuses
APPLE_IAP_BUNDLE_ID and the existing service-account credentials.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 16:48:17 +02:00
Huang Xin 9e163fe746 fix(payment): reflect highest active plan across overlapping Stripe subscriptions (#4694)
* fix(payment): reflect highest active plan across overlapping Stripe subscriptions

When a user upgrades Plus to Pro, both subscriptions stay active until the old
one is cancelled. Each subscription webhook overwrote plans.plan with only that
event's plan, so whichever webhook arrived last won and could downgrade the
account back to plus.

Derive plans.plan from the highest active (or trialing) subscription via a new
getHighestActivePlan helper, used by createOrUpdateSubscription and by the
cancellation handler so a still-active higher plan is preserved instead of
dropping to free.

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

* test(payment): add opt-in live Stripe test for getHighestActivePlan

Skipped by default (CI included); runs only when STRIPE_SECRET_KEY and
STRIPE_TEST_CUSTOMER_ID are set, so it can be exercised locally against a real
customer with overlapping subscriptions. The module under test is imported
dynamically so the file stays import-safe while skipped.

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 07:29:39 +02:00
Huang Xin 86f5502724 fix: bot-review robustness fixes (TTS sync, updater, nightly, a11y) (#4659)
Cherry-picked and re-verified the applicable subset of
julianshen/readest@fa1b74a0 (its "address PR #15 bot reviews" commit). The
fork-only AI-annotation-tool change was dropped — readest has no 'ai' toolbar
tool. Each logic fix is covered by a failing-first test.

- TTS position sequence is now an app-wide monotonic counter, so a fresh
  TTSController (constructed per `tts-speak`) isn't dropped by consumers holding
  `lastSequenceSeen` from a prior session.
- share.ts only swallows AbortError (user cancel); other failures — e.g.
  NotAllowedError when a quick action fires without a user gesture — fall back to
  the clipboard so the text still reaches the user.
- document.isTxt tolerates MIME params (text/plain;charset=utf-8), uppercase
  extensions (BOOK.TXT), and a nameless Blob, so a TXT can't slip onto the
  non-text path and yield a null book.
- updater getNightlyPlatformKey matches x86_64/aarch64 explicitly; a 32-bit or
  otherwise unknown arch yields no nightly instead of mis-routing to aarch64.
- UpdaterWindow downloadWithProgress resolves on tauriDownload completion even
  when Content-Length is absent (no more hang on portable/AppImage/Android).
- nightly_update.rs uses async tokio::fs::read in the async command.
- nightly.yml: serialize runs via a concurrency group (no cancel) and
  persist-credentials:false on checkouts.
- edge TTS route only emits the word-boundary header when it fits under ~8KB;
  oversized values get dropped by proxies, and the client falls back to [].
- RSVPOverlay drops the contradictory aria-disabled on the functional rate
  button (it opens the pace picker).
- nightly verify harness handles artifact stream errors instead of crashing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:04:34 +02:00
Huang Xin 495783d045 fix(security): harden OPDS proxy SSRF, storage key validation, Stripe check (#4638)
Server-side hardening for three reported web advisories:

- OPDS proxy (/api/opds/proxy): add http(s) scheme allowlist, internal/loopback/
  link-local host blocklist, and manual per-hop redirect re-validation so a
  public URL can't redirect into an internal address. Move isBlockedHost into
  the shared src/utils/network.ts as the canonical blocklist and reimplement
  isLanAddress to delegate to it (also tightens the /api/kosync LAN check);
  fetch-url.ts re-exports it. (GHSA-c7mm-g2j2-98cx, GHSA-5g3f-mq2c-j65v)

- Storage upload (/api/storage/upload): validate the client-supplied fileName
  with a new isSafeObjectKeyName helper before building the object key, so a
  name can't escape the caller's own prefix. (GHSA-mfmj-2frf-vhgw)

- Stripe (/api/stripe/check): bind the entitlement to the session owner —
  reject a Checkout Session whose metadata.userId differs from the authenticated
  caller. (GHSA-pv88-3727-j7v8)

Unit tests added for each path; full suite + lint green. The Tauri-native
advisory (GHSA-55vr-pvq5-6fmg) is handled in a separate change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:45:29 +02:00
Huang Xin a56cc6c61a feat(tts): word-by-word highlighting for Edge TTS, closes #4017 (#4566)
Highlight each word as it is spoken (Edge TTS only) instead of keeping the
whole sentence highlighted, and keep the view tracking the spoken word
across page boundaries.

Word boundaries
- Capture Edge's audio.metadata WordBoundary frames (offset/duration in
  100ns ticks plus the verbatim input-text span) in the Tauri, browser, and
  Cloudflare-Workers WebSocket transports.
- Carry boundaries through the authenticated HTTPS proxy route via an
  X-TTS-Word-Boundaries response header (percent-encoded JSON, ASCII-safe),
  so word highlighting works on the web where the browser cannot open the
  wss connection directly. Cache them alongside the audio blob URL.

Highlighting
- Sync a requestAnimationFrame loop to audio.currentTime against the
  boundary table and highlight the word sub-range within the spoken
  sentence. Synthesis stays sentence-level (natural prosody); only the
  visual highlight is word-level.
- Suppress the sentence highlight when the active client reports word
  boundaries and draw the first word immediately, so the whole sentence
  never flashes before the first word. Fall back to the sentence highlight
  when a chunk has no boundaries (other engines, empty metadata).
- Re-apply the current word (not the sentence) when the view relocates.

Page following
- Turn the page as soon as the spoken word crosses a page boundary (a
  tts-highlight-word event scrolls only when the word is outside the visible
  range), instead of waiting for the next sentence.
- Check the word's position for the "back to TTS location" badge so it no
  longer appears while the view follows the word onto the next page.

Also fixes a pre-existing bug where the browser WebSocket was constructed
with an options object (valid only for the Node ws package), which threw in
browsers and made the wss path unusable on the web.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 08:29:34 +02:00
Huang Xin 3b348c8f35 feat(sync): CRDT replica sync foundation (#4075)
* feat(sync): foundation for CRDT-based cross-device replica sync (Phase 1+2)

Adds the primitives and orchestration layer for syncing user-imported
assets (dictionaries, fonts, textures, OPDS catalogs, dict settings)
across devices via a polymorphic `replicas` table with field-level LWW
under HLC ordering. Phase 1 ships the foundation (CRDT, crypto, server
schemas, SQL migrations, push/pull endpoint); Phase 2 adds the adapter
registry, HTTP client, and sync manager. No modifications to existing
book sync — additive only.

Phase 1:
- src/libs/crdt.ts — HlcGenerator (monotonic + remote-absorption +
  clock-regression-safe), per-field LWW with deviceId tiebreak,
  remove-wins tombstones, reincarnation token revival.
- src/libs/crypto/{derive,encrypt,envelope,passphrase}.ts —
  PBKDF2-600k key derivation (OWASP 2024), AES-GCM round-trip,
  envelope {c,i,s,alg,h} with SHA-256 sidecar integrity check,
  passphrase storage abstraction (web ephemeral; Tauri keychain stub).
- src/libs/replica-schemas.ts — Zod-backed allowlist (dictionary only
  in PR 1), 64KiB row cap, 64-field cap, schemaVersion bounds,
  filename validator.
- src/libs/replica-sync-server.ts — push batch validation
  (auth + allowlist + schema + HLC ±60s skew clamp).
- src/pages/api/sync/replicas.ts — POST/GET endpoint wrapping the
  Postgres crdt_merge_replica function via RPC.
- docker/volumes/db/migrations/003_add_replicas.sql — replicas table
  + replica_keys table + RLS.
- docker/volumes/db/migrations/004_crdt_merge_replica_fn.sql — atomic
  per-field LWW merge function (forwards-compat preserves unknown
  fields).

Phase 2:
- src/services/sync/replicaRegistry.ts — adapter contract
  (core + optional BinaryCapability + LifecycleHooks per eng review).
- src/libs/replica-sync-client.ts — HTTP wrapper mapping status codes
  to typed SyncError codes.
- src/services/sync/replicaSyncManager.ts — 5s debounced push,
  immediate flush on visibilitychange/online, per-kind pull cursor,
  remote HLC absorption.

Tests: 125 new (crdt 26, crypto 32, schemas 21, server 16, client 12,
registry 6, manager 12). Full suite 3656 passing, lint clean. Existing
book/config/note sync paths untouched.

Plan: ~/.claude/plans/vivid-orbiting-thimble.md
CEO plan: ~/.gstack/projects/readest-readest/ceo-plans/2026-05-06-replica-sync-cathedral.md

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

* feat(sync): add kind="replica" path through TransferManager (Phase 3)

Adds the replica branch to the existing book-shaped transfer
infrastructure so dictionary (and future kinds) bundles can flow through
the same queue, retry, and progress UI as book uploads.

Existing book transfer paths remain unchanged. The book-side regression
suite (37 tests in transfer-store.test.ts, 37 in transfer-manager.test.ts)
all stay green.

Store (src/store/transferStore.ts):
- TransferItem gains kind: 'book' | 'replica' (default 'book' on legacy
  persisted rows), replicaKind, replicaId, replicaFiles, replicaBase.
- New addReplicaTransfer(replicaKind, replicaId, displayTitle, type, opts)
  with files + base in opts; auto-computes totalBytes from file sizes.
- New getReplicaTransfer(replicaKind, replicaId, type) lookup.
- getTransferByBookHash filters to kind === 'book' (defensive against
  bookHash="" collisions on replica items).
- restoreTransfers fills kind: 'book' for legacy persisted rows.

Manager (src/services/transferManager.ts):
- queueReplicaUpload / queueReplicaDownload / queueReplicaDelete.
- executeTransfer dispatches by kind to the new executeReplicaTransfer
  (iterates files, calls appService.uploadReplicaFile per file with
  per-file progress aggregation) or the existing executeBookTransfer
  (refactored out, byte-identical behavior).
- Dispatches replica-transfer-complete event on success so stores can
  react (e.g., commit manifest_jsonb to the replica row).

Storage / cloud (src/libs/storage.ts, src/services/cloudService.ts):
- uploadReplicaFile bypasses the book-only File.name smuggling and
  takes an explicit cfp (cloud file path).
- uploadReplicaFileToCloud / downloadReplicaFileFromCloud /
  deleteReplicaBundleFromCloud orchestrate per-file operations under
  ${userId}/Readest/replicas/<kind>/<replicaId>/<filename>.
- replicaCloudKey() centralizes the path-construction rule.
- New CLOUD_REPLICAS_SUBDIR constant.

App service (src/services/appService.ts, src/types/system.ts):
- AppService gains uploadReplicaFile, downloadReplicaFile,
  deleteReplicaBundle (file-level operations; orchestration lives in
  TransferManager).

Tests: 18 new (12 in transfer-store.test.ts, 5 in transfer-manager.test.ts,
1 fixture). Full suite 3674 passing, lint clean. Existing book regression
clean.

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

* feat(sync): dictionary replica adapter + bootstrap (Phase 4, partial)

Lands the safe-to-ship foundation of Phase 4 — adapter logic, registry
bootstrap, and SystemSettings hooks for replica sync. The on-disk
migration of legacy customDictionaries (bundleDir → content-hash id),
the live store wiring, and the Settings → Sync UI are deferred to a
follow-up PR so they can land with real-device QA.

Adapter (src/services/sync/adapters/dictionary.ts):
- dictionaryAdapter: kind='dictionary', schemaVersion=1.
- pack/unpack — only synced subset (name, kind, lang, addedAt,
  unsupported{,Reason}). bundleDir / files / unavailable / deletedAt
  stay per-device or are handled by the tombstone mechanism.
- BinaryCapability.enumerateFiles dispatches by bundle kind:
  - mdict:    mdx + mdd[] + css[]
  - stardict: ifo + idx + dict + syn (skips .idx.offsets / .syn.offsets
              sidecars — those are device-local indices)
  - dict:     dict + index
  - slob:     single .slob file
- primaryDictionaryFile() picks the anchor file per kind for
  partialMD5 hashing.
- computeDictionaryReplicaId(partialMd5, byteSize, sortedFilenames)
  produces a deterministic 32-hex content-hash id used at import time.
- 23 tests cover pack/unpack identity, kind dispatch, file enumeration,
  id determinism, and per-device-field exclusion.

Bootstrap (src/services/sync/replicaBootstrap.ts):
- bootstrapReplicaAdapters() registers all known adapters once at app
  start. Idempotent (safe to call multiple times). Wired into
  EnvContext.tsx so the registry populates on app mount.
- 3 tests cover registration, idempotency, and the PR-1 allowlist.

SystemSettings (src/types/settings.ts, src/services/constants.ts):
- +SyncCategory = 'book' | 'progress' | 'note' | 'dictionary' — typed
  union for the user-facing sync toggles. 'progress' gates the
  existing book-config sync (reading progress); 'note' gates
  annotations; 'book' gates book binaries + metadata; 'dictionary'
  gates the new replica sync. Future replica kinds extend the union.
- +SYNC_CATEGORIES readonly array for UI iteration.
- +syncCategories: Partial<Record<SyncCategory, boolean>> — per-
  category opt-in toggles in DEFAULT_SYSTEM_SETTINGS (default ON for
  all four). UI panel ships in the follow-up.
- +lastSyncedAtReplicas: Record<string, string> — per-kind HLC pull
  cursors (matches replicaSyncManager's CursorStore contract).

Registry type cleanup (src/services/sync/replicaRegistry.ts):
- BinaryCapability.enumerateFiles return shape: localRelPath → lfp
  to match the existing TransferStore.ReplicaTransferFile convention.

Tests: 28 new (23 dict + 3 bootstrap + 2 syncCategories defaults).
Full suite 3702 passing, lint clean. Existing book/config/note sync
paths untouched.

Deferred to PR 1 follow-up (with real-device QA):
- customDictionaryStore migration: rehash legacy uniqueId() bundleDir
  to content-hash id; preserve providerOrder mapping; staged
  .legacy/<old-id>/ backup.
- Wire customDictionaryStore mutations through replicaSyncManager
  (markDirty on add/rename/delete; pull on init).
- Settings → Sync panel: per-category toggles + last-sync timestamps.
- Sync passphrase modal: set / change / forgot flow (lazy first prompt
  on encrypted-field push/pull).
- <CloudReplicaRow> in CustomDictionaries.tsx for "Download from cloud
  (X MB)" affordance.
- Tauri keychain backend for sync passphrase storage.

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

* feat(sync): replicaSync singleton + content-hash id at dict import (Phase 4b)

Two foundation pieces that everything UI-side will sit on top of, both
purely additive — no live store wiring, no behavior change for existing
dictionary imports.

replicaSync singleton (src/services/sync/replicaSync.ts):
- initReplicaSync({deviceId, cursorStore, hlcStore?, client?}) builds
  one ReplicaSyncManager backed by an HlcGenerator with persistence
  wrapped around .next()/.observe(). Idempotent (second init returns
  the existing instance).
- LocalStorageHlcStore (src/libs/hlc-store.ts) snapshots the HLC
  counter under 'readest_replica_hlc' so it survives restart. Falls
  back silently when localStorage is unavailable (private mode, SSR);
  client re-derives via the existing remote max(updated_at_ts) repair
  path. InMemoryHlcStore is the test backend.
- 16 tests (9 hlc-store, 7 replicaSync) covering snapshot persistence,
  restore-on-init, and idempotency.
- Wiring into EnvContext for production deferred to the follow-up that
  also adds the cursor store backed by useSettingsStore.

Content-hash id at dictionary import
(src/services/dictionaries/contentId.ts):
- computeDictionaryContentId(primaryFile, filenames) wraps
  computeDictionaryReplicaId(partialMd5(primary), byteSize,
  sortedFilenames) — the cross-device id used as the replica_id when
  the dict actually pushes/pulls.
- Wired into all four import paths in dictionaryService.ts:
  - stardict primary = .ifo (small text, partialMD5 ≈ full hash)
  - mdict primary    = .mdx (body)
  - dict primary     = .dict.dz (gzipped body)
  - slob primary     = .slob (single-file bundle)
  ImportedDictionary gains contentId?: string. Optional for backwards
  compat; legacy bundles without contentId are flagged as
  "needs rehash before sync" by the upcoming store-wiring follow-up.
- 6 tests cover identity determinism, byteSize sensitivity, filename-
  set sensitivity, and order-independence.

Tests: 22 new (9 + 7 + 6). Full suite 3724 passing, lint clean.
Existing dictionary import flow unchanged for users — contentId is an
additional field, not a replacement for the bundleDir-based id.

Deferred to follow-up (with on-device QA):
- Production cursor store backed by useSettingsStore +
  appService.saveSettings.
- EnvContext call to initReplicaSync after appService boot.
- customDictionaryStore mutation hooks → replicaSyncManager.markDirty.
- Legacy bundleDir → contentId migration with .legacy/ backup.

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

* docs(sync): land replica-sync design plan in repo

Moves the plan document that drove this PR's foundation work
(`~/.claude/plans/vivid-orbiting-thimble.md`) into the project tree at
`apps/readest-app/.claude/plans/` so reviewers and future contributors
can read it alongside the code without leaving the repo.

The plan went through three review passes — Codex (19 findings, all
absorbed), CEO/scope review (mode SCOPE EXPANSION; encrypted secrets
pulled forward to v1, "private-only forever" posture lock), and eng
review (FULL_REVIEW mode, 16 findings absorbed). The full review trail
lives in the file's `## GSTACK REVIEW REPORT` section at the bottom.

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

* docs(crdt): point README at in-repo plan path

Now that vivid-orbiting-thimble.md lives at
apps/readest-app/.claude/plans/, the README link should point there
rather than at the home-dir copy that no longer exists.

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

* refactor(sync): rename src/libs files to camelCase per project convention

Test files renamed in lockstep. Imports + comment references updated
across cloudService, storage, transferManager, replicaSync,
replicaSyncManager, /api/sync/replicas, and all four test files. No
behavior 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>
2026-05-06 15:50:15 +02:00
Huang Xin 19f2414f42 fix(share): hide download link on share landing page (#4041)
Direct file download from the public share landing carries rights /
abuse risk. Replace the Download button with the Open-in-app deep link
in both the logged-in flow (now: Add to library + Open in app) and the
anonymous flow (now: Open in app + Get Readest footnote).

The /api/share/[token]/download route is left intact so re-enabling
the button later is a one-file UI change.
2026-05-03 06:06:11 +02:00
Huang Xin 19f3e65b62 fix(share): make /s landing build under Next 16 layout-prop validation (#4040)
* fix(share): make /s landing build under Next 16 layout-prop validation

Production builds (`next build --webpack` for OpenNext on Cloudflare)
rejected the Layout default export with "Type 'LayoutProps' is not valid"
because Next 16 strictly enforces that layout components only accept
`{ children }` (and `{ params }` for dynamic segments) — never
`searchParams`. The previous design tried to read `searchParams` from
both the layout component AND its `generateMetadata`, but layouts don't
get `searchParams` at all (they're shared across child pages with
potentially different query strings).

Restructure:
- Move `generateMetadata` from `app/s/layout.tsx` to `app/s/page.tsx`,
  which DOES receive `searchParams`. Page is now a server component.
- Split the existing client component into `app/s/ShareLanding.tsx`
  (still `'use client'`); page.tsx wraps it in `<Suspense>` per the
  Next 16 client-component contract.
- Delete `app/s/layout.tsx` — no longer needed; the project's root
  layout still wraps everything.

Verified locally: `pnpm lint` clean, `pnpm build-web` green, all 9
share API routes plus `/s` show up in the route manifest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(share): drop edge runtime from og.png so OpenNext can bundle it

OpenNext on Cloudflare errors out when bundling edge-runtime routes inside
the default server function:

  app/api/share/[token]/og.png/route cannot use the edge runtime.
  OpenNext requires edge runtime function to be defined in a separate
  function.

Splitting into a second function bundle is more config surgery than this
route deserves. `next/og`'s `ImageResponse` (Satori + WASM yoga/resvg) has
supported the default Node-compat runtime since Next 13.4, and on
Cloudflare via OpenNext the default function IS already a Worker, so
cold-start cost is similar to edge.

Verified: `pnpm exec opennextjs-cloudflare build` now completes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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-03 04:40:16 +02:00
Huang Xin d1e7b4902c feat(share): time-limited share links with cfi-aware imports (#4037)
Add a Share Book feature that generates an expiring HTTPS share URL plus a
parallel readest://share/{token} deep link. Recipients land on /s/{token},
where logged-in users can one-tap "Add to my library" (R2 server-side
byte-copy) and anonymous users download the book or open it in the app.
Sharers manage active links from a dedicated "Manage Shared Links" panel
under user settings.

Highlights:
- 9 new App Router endpoints under /api/share (create, [token], cover,
  og.png, download, download/confirm, import, revoke, list).
- /s landing page with branded next/og chat unfurl image and SSR auth-cookie
  detection so logged-in recipients see "Add to my library" as the primary
  action without layout shift.
- Server-side R2 byte-copy for /import preserves the project's existing
  invariant that every files.file_key is prefixed with its row's user_id;
  stats / purge / delete / download routes work unchanged. URL-encodes the
  copy source so titles with spaces or '&' don't break the copy.
- Universal 7-day expiry cap, no tier differentiation, no "never". DMCA-risk
  reduction. Picker defaults to 3 days.
- Position-aware shares: "Share current page" toggle (off by default for
  privacy) attaches the sharer's CFI; recipient lands at the same paragraph.
- Per-user 50-share cap, rate limiting via Cache-Control: no-store on
  token-bearing responses, atomic SQL increment for download_count via a
  SECURITY DEFINER function so the public confirm beacon stays safe under
  concurrent fire.
- Soft revocation: presigned download URLs (5-min TTL) cannot be cancelled
  before TTL; documented as accepted v1 behavior.
- token + token_hash hybrid storage: public endpoints look up by hash and
  never select the raw token, so accidental SELECT-* leakage on a public
  route can't expose the bearer credential.
- Mobile / desktop Tauri share via tauri-plugin-sharekit; web falls back to
  navigator.share with a clipboard fallback when no native share method
  exists. Share-sheet dismissal no longer silently copies.

UI:
- New Dialog with a settings-card group: iOS-style segmented duration picker
  + toggle slider for "Share current page", on a single row each.
- Reader top-bar Share button, library context-menu Share entry, manage-
  shares list with cover thumbnails and overflow menu.
- New <SegmentedControl> primitive in src/components for reuse.

Coverage:
- Unit tests for token utils + URL parser (20 new tests, full suite at 3445).
- 31 locales translated for all new strings; en plurals hand-added per the
  project's hand-curated en convention.

DB migration in docker/volumes/db/migrations/002_add_book_shares.sql adds
the book_shares table, RLS policies, and the increment_book_share_download
RPC. Migration is idempotent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:03:35 +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 38d7ba80fe feat(opds): support auto-download books from OPDS feeds (#3844)
* feat: auto-download new items from subscribed OPDS catalogs

Add opt-in auto-download per OPDS catalog. When enabled, new publications
are downloaded on app startup and pull-to-refresh. Dedup via Atom <id>.

- Extend foliate-js parser to surface entry id/updated fields
- Add subscription state to OPDSCatalog type
- New headless opdsSyncService with navigation feed crawling
- Auto-download toggle in CatalogManager UI
- Wire sync into startup and pull-to-refresh hooks
- 16 new test cases

Built with an AI code agent.

Closes #3836

* chore: point foliate-js submodule to fork with OPDS id/updated fields

CI needs to fetch the foliate-js commit that adds id and updated field
extraction from OPDS feeds. Point submodule URL to our fork where the
commit is pushed.

* feat(opds): rearchitect auto-download with two-phase OPDS sync

Replaces the monolithic opdsSyncService from #3837 with a separated
discovery + acquisition pipeline. Subscription state lives in
per-catalog JSON files under Data/opds-subscriptions/, decoupled from
catalog config.

Discovery (services/opds/feedChecker.ts) resolves a catalog URL down
to its "by newest" feed via three detection tiers — rel="http://opds-
spec.org/sort/new" (Calibre / Calibre-Web), title heuristics
("Newest", "Recently Added", "Latest"), and href patterns
(?sort_order=release_date for Project Gutenberg, /new-releases for
Standard Ebooks). It then walks only that feed plus rel=next
pagination, never the full navigation tree. When no by-newest feed is
found, the catalog is skipped with a warning instead of silently
mirroring everything.

Acquisition link selection filters to safe rels
(acquisition / acquisition/open-access — drops buy / borrow /
subscribe / sample / indirect), prefers open-access when both rels
are present, then ranks by format tier:
  0  Advanced EPUB / EPUB 3 (link title, .epub3 href, version=3.x)
  1  plain EPUB
  2  MOBI / AZW / AZW3
  3  PDF / CBZ
  4  anything else
Within a tier, ties resolve by feed order. When the upstream omits or
mis-declares (octet-stream) the link's media type, format is inferred
from href extension and link title. Entries that appear in both
feed.publications and a group are deduped within the same batch.

Orchestration (services/opds/autoDownload.ts) runs catalogs
sequentially — per-catalog concurrency already caps parallel downloads
at 3, so a parallel fan-out across catalogs would be N×3 simultaneous
requests on cellular. Pending and retry items are deduped by entryId,
and entries still inside their exponential-backoff window are skipped
instead of re-attempted (avoids appending duplicate failedEntries
records). Filenames come from the last path segment, capped at 200
chars, with a try/catch around decodeURIComponent for malformed
%-sequences.

Hook (hooks/useOPDSSubscriptions.ts): startup, 5-minute interval, and
'check-opds-subscriptions' event triggers. Toggling auto-download on
fires the event so the sync runs immediately. Newly imported books
are queued for cloud upload via transferManager when the user is
logged in and settings.autoUpload is on, mirroring the manual OPDS
download path. 'opds-sync-complete' is dispatched after every sync so
listening UI can refresh without polling. loadSubscriptionState
self-heals duplicate failedEntries from older state files.

The submodule URL is reverted to readest/foliate-js (the changes
needed for OPDS id/updated extraction are already in main), and
OPDSCatalog is trimmed to only the autoDownload field.

Verbose [OPDS]-prefixed debug logs cover the whole pipeline: feed
fetch → resolve → discovery → per-item download (with magic-byte
dump of the saved file) → import → cloud upload queueing.

34 new tests across opds-feed-checker, opds-auto-download,
opds-subscription-state, opds-types.

Closes #3836.
Supersedes #3837.

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

---------

Co-authored-by: Johannes Mauerer <johannes@mauerer.info>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:04:32 +02:00
Yi-Shun Wang 45bd355981 feat(opds): support custom catalog headers with web proxy consent (#3740)
* Add custom headers support for OPDS catalogs

* Require web proxy consent for sensitive OPDS credentials

* Strengthen OPDS header forwarding tests

---------

Co-authored-by: Yi-Shun Wang <git@albertwang.dev>
2026-04-04 12:01:16 +02:00
AK Venugopal 80cab8e56d feat: Hardcover.app Sync (#3724)
* feat(hardcover): add one-way Hardcover sync integration

- Add HardcoverClient with ISBN/title-based book lookup, progress push, and note sync
- Add HardcoverSyncMapStore for persistent local mapping of note IDs to Hardcover journal IDs
- Add GraphQL queries/mutations for Hardcover API (insert/update/recreate journal entries)
- Add DB migration for hardcover_note_mappings table (schema: hardcover-sync)
- Add HardcoverSettings dialog component (connect/disconnect/enable toggle)
- Add useHardcoverSync hook wired in Annotator for push-notes and push-progress events
- Add Hardcover Sync section in BookMenu with per-book toggle (persisted in BookConfig)
- Add HardcoverSettings type and DEFAULT_HARDCOVER_SETTINGS to system settings
- Add hardcoverSyncEnabled per-book flag to BookConfig
- Mount HardcoverSettingsWindow in Reader alongside KOSync and Readwise windows

Sync is one-way (Readest → Hardcover), manual-action only, per-book opt-in.
Supports idempotent note sync: insert new, skip unchanged, update changed, recreate on stale remote ID.

* fix(hardcover): proxy API calls server-side to fix CORS, normalize Bearer token

- Add /api/hardcover/graphql Next.js route that proxies POST requests server-side,
  bypassing CORS restrictions (Hardcover API has no Access-Control-Allow-Origin header)
- On Tauri (desktop), calls Hardcover directly; on web, routes through the proxy
- Normalize token in HardcoverClient: accept raw JWT or 'Bearer <jwt>' format
- Update helper text to point to hardcover.app → Settings → API

* fix(hardcover): surface note-sync no-ops and harden book resolution

- Show toast feedback when book data is still loading, Hardcover is not configured,
  or the current book has no annotations/excerpts to sync
- Show an explicit info toast when note sync finds no new changes
- Parse Hardcover search results in the current hits/document response shape
- Resolve note sync through ensureBookInLibrary for parity with progress sync
- Add console logging for note/progress sync failures

* debug(hardcover): add runtime instrumentation for note sync

* feat(metadata): keep identifier stable and store ISBN separately

- Add dedicated metadata.isbn field
- Expose ISBN as its own editable field in book metadata
- Preserve identifier semantics for existing source IDs and hashes
- Route metadata auto-retrieval ISBN handling through the new field
- Prefer metadata.isbn for Hardcover matching

* fix(hardcover): avoid wasm sqlite for note mappings on web

- Store Hardcover note sync mappings in localStorage on web
- Keep sqlite-backed mappings for desktop/native environments
- Remove the web-only database dependency from manual note sync

* fix(hardcover): dedupe notes by payload hash across unstable note IDs

- Add payload-hash lookup in HardcoverSyncMapStore
- Reuse existing journal mapping when payload already synced
- Prevent duplicate insertions when note IDs change or duplicate locally

* fix(hardcover): avoid duplicate quote export when annotation note exists

- Detect excerpt+annotation pairs for the same highlight
- Skip standalone excerpt export when annotation has note text
- Keep annotation export as the single source of truth

* docs(hardcover): add consolidated change summary for review

* fix(hardcover): suppress excerpt export by CFI when annotation note exists

* fix(hardcover): suppress empty-note annotation duplicates when note exists

* fix(hardcover): deduplicate notes by text and cfi base node

* refactor(hardcover): optimize sync performance, add rate limiting and clean up debug tools

* chore: remove dev-only change log from main

* test(hardcover): add unit tests for sync mapping and client logic

* chore: custom deployment and UI fixes

* fix(hardcover): use timestamptz for accurate annotation time

* fix(hardcover): use date scalar and RFC 3339 formatting for journal entries

* Revert "chore: custom deployment and UI fixes"

This reverts commit 0329aba7129d1e1ebf2c663804b8fba9a9f87b91.

* Fix hardcover progress dates and surface imported ISBNs

* Fix Hardcover currently reading sync

* fix(hardcover): avoid promoting note sync status

* style(hardcover): apply prettier formatting

* test(hardcover): fix strict TypeScript assertions

* test(hardcover): harden sync regression coverage

* test(hardcover): fix lint and formatting regressions

* fix(hardcover): narrow note dedupe range matching

* refactor(hardcover): extract note dedupe helpers

* refactor(isbn): extract metadata normalization helpers

* feat(hardcover): improve synced quote formatting
2026-04-03 09:06:43 +02:00
Huang Xin db3dee025b feat(booshelf): dedupe books with the same meta hash (#3535) 2026-03-14 15:14:22 +01:00
Huang Xin 8a468a6d1f fix(opds): more robust parsing for authors in metadata, closes #3120 (#3134) 2026-02-01 15:57:10 +01:00
Mohammed Efaz 5bbc5ceccc feat(ai): AI reading assistant phase 2 (#3023)
* feat(ai): add dependencies

* chore: bump zod version to default version

* feat(ai): define types and model constants

* feat(ai): ollama provider for local LLM

* feat(ai): implement openrouter provider for cloud models

* feat(settings): register ai settings panel in global dialog

* refactor(ai): expose provider factory and service layer entry point

* test(ai): add unit tests for the providers

* test(ai): add unit tests for the providers

* feat(ai): settings panel for ai configurations

* refactor(ai): rewrite aipanel with autosave and greyed out disabled state

* fix: remove unused onClose prop from aipanel

* test(ai): update mock data

* refactor(ai): remove models

* refactor: use centralised defaults in system defaults

* chore(ai): remove comments

* fix(ai): merge default ai settings on load to prevent undefined values

* refactor(ai): rewrite settings panel with autosave and model input

* feat(ai): add ai tab with simplified highlighting

* feat(sidebar): render AIAssistant for ai tab

* feat(ai): add chat UI

* feat(ai); add chat service with RAG context

* feat(ai): temp debug logger

* feat(ai): add RAG service

* feat(ai): add text chunking utility

* feat(ai): add structured method

* feat(ai): add chatstructured method

* feat(ai): add rag types nd structured output schema

* feat(ai): add aistore, indexdb, bm25

* fix: update lock file

* feat(ai): update types for AI SDK v5

* feat(ai): add placeholder gateway model constants

* refactor(ai): update OllamaProvider for AI SDK

* feat(ai): add native gateway provider

* refactor(ai): update provider exports

* refactor(ai): use streamText from AI SDK

* refactor(ai): use embed from AI sdk

* refactor(ai): update provider factory exports

* feat(ai): add AI Elements and shadcn components

* config: add shadcn component config

* deps: add AI SDK and AI Elements dependencies

* config: add ai packages to transpilePackages

* refactor(ai): remove OpenRouterProvider and old tests

* feat(ai):add assistant-ui components

* feat(ai): add TauriChatAdapter for assistant-ui runtime

* refactor(ai): remove ai-elements components

* dep(ai): install assistant-ui and update next config

* chore(ai): export adapters from service index

* feat(ui): enhance ui components for assistant integration

* feat(settings): migrate ai settings to gateway

* feat(sidebar): integrate assistant-ui

* feat: add ai settings toggle to sidebar content

* feat: conditionally show ai tab in sidebar navigation

* feat: update ai model constants for cheaper options

* feat: add gateway provider with proxied embedding

* feat: add timeouts to ollama provider health checks

* feat: add retry logic to rag service embeddings

* feat: add error recovery to ai store

* feat: add ai feature tests

* feat: add ai api endpoints

* feat: add proxied gateway embedding provider

* feat: add ai runtime utilities

* feat: add ai retry utilities

* feat: add tauri env example template

* feat: add web env example template

* chore: add env

* feat(ai): update models and pricing, remove GLM-4.7-FlashX

* feat(ai): improve system prompt with official headings and no numeric citations

* feat(ai): optimize system prompt for tauri chat

* feat(ui): refine ai chat UI and relocate sources

* feat(ui): update ai settings panel with model pricing and custom model support

* feat(ai): add custom model support to ai settings

* test(ai): update constants tests for removed model

* feat(api): implement ai chat proxy route

* feat(api): implement ai embedding proxy route

* feat(ai): implement ai gateway health check and proxy logic

* feat(ai): simplify proxied embedding provider

* feat(ui): improve markdown text rendering

* feat(ui): add input group component

* test(ai): update ai provider tests

* feat(ai): add pageNumber to text chunk schema

* feat(ai): implement page-based chunking with 1500 char formula

* feat(ai): bump db to v2 and add store reset migration

* feat(ai): transition rag pipeline to page level spoiler filtering

* feat(ai): overhaul readest persona and antijailbreak prompt

* feat(ai): update tauri adapter for page tracking and persona

* chore(ai): export aiStore and logger from core index

* feat(reader): integrate page tracking and manual index reset

* feat(ui): add re-index action and reset logic to chat

* chore: sync pnpm lockfile with ai dependencies

* feat(utils): add browser-safe file utilities for web builds

* refactor(utils): use dynamic tauri fs import to prevent web crashes

* refactor(services): defer osType call to init() for web compatibility

* refactor(services): import RemoteFile from file.web

* refactor(services): import ClosableFile from file.web

* fix(libs): cast Entry to any for getData access

* fix(annotator): cast Overlayer to any for bubble access

* refactor(ai): replace SparklesIcon with BookOpenIcon for index prompt

* test(ai): add pageNumber to TextChunk mocks

* test(ai): fix chunkSection signature in tests

* chore: update files

* fix(ai): prevent useLocalRuntime crash when adapter is null

* refactor: optimize annotator overlay drawing

* feat: stabilize AI assistant runtime and adapter

* refactor: improve document zip loader type safety

* feat: update tauri chat adapter for dynamic options

* fix: restore architecture comments and refine platform properties

* build: update lockfile with assistant-ui patch

* fix(library): patch @assistant-ui/react for runtime initialization

* build: update dependencies in readest-app

* build: update root dependencies and patch configuration

* fix(ai): patch @assistant-ui/react for thread deletion and runtime init

* fix(ai): update assistant-ui patch with dist guards and deletion fallback

* build: sync lockfile with assistant-ui patch updates

* chore(env): update .gitignore by removing .env files from it

* chore(env): update .gitignore by adding .env.local

* chore(env): update .gitignore by adding .env*.local

* fix: restore static osType import

* chore: sync submodules with upstream/main

* refactor: remove redundant file.web module and revert import

* chore: update pnpm-lock.yaml

* refactor: revert guards

* refactor; remove deprecated codes and extract prompts.ts

* refactor(ai): remove unused ragservice exports

* refactor: remove unused ollama and embedding models

* refactor: remove unused type

* test: remove test for the now deleted constants

* refactor: remove unused export

* style: fix ui component formatting

* style: fix core and style file formatting

* test: fix broken ai provider import

* fix: typescript error

* fix: add eslint disable command

* fix(deps): remove unused ai sdk provider util after v6 ai sdk migration

* fix(patch): add lookbehind regex patch

* feat(dep): upgrade vercel ai sdk to v6 and ai-sdk-ollama to v3

* chore: update lockfile for vercel ai sdk v6

* refactor(ai): remove EmbeddingModel generic for ai sdk v6

* refactor(ai): remove EmbeddingModel generic for ai sdk v6

* test(ai): update mock to use embeddingModel

* fix(patch): add lookbehind regex patch for email autolinks in markdown

* refactor(ai): use ai sdk v6 syntax

* fix: prettier formatting

* chore: revert cargo.lock

* fix(ai): update proxied embedding model to v3 spec

* feat(ai): add aiconversation types for chat persistence

* feat(ai): add conversation/message indexeddb and crud operations

* feat(ai): create aiChatStore zustand store for chat state management

* feat(notebook): add notebookactivetab state for Notes/AI

* refactor(ai): refine conversation and message types for persistence

* feat(types): add notebookActiveTab to ReadSettings type

* chore: update deps

* feat: add notebookactive tab default value

* feat: add hook for ai chat

* feat: update left side panel with history/chat icon

* feat: integrate ChatHistoryView into sidebar content

* feat: create UI for managing AI chat history

* feat: implement persistent history with assistant-ui adapter

* feat: create tab navigation component for notes and AI

* feat: add tab navigation and AI assistant view

* feat: update header to display active tab title

* fix: formatting

* feat: remove title and update new chat button

* fix: formatting

* fix: revert tooltip and styling

* feat: implement cross-platform ask dialog bridge

* feat(ai): preserve history during ui clear & use native dialogs

* fix: align notebook navigation height with sidebar tabs

* fix(ai): add missing dependency to handleDeleteConversation hook

* docs: update PROJECT.md with session highlights

* chore: delete projectmd

* chore: update package.json and lock file

* chore: update package.json

* chore: remove patch

* chore: upgrade react types to 19 and show ai features only in development mode for now

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-24 11:38:48 +01:00
Huang Xin 2d7d6b08a9 compat(opds): parse attachment filename from download requests, closes #2969 (#2976) 2026-01-16 17:13:30 +01:00
Huang Xin aaee04c290 fix(opds): probe filename when downloading PDFs and CBZ files, closes #2921 (#2932) 2026-01-12 17:55:50 +01:00
Huang Xin ca8d25341e compat(opds): support Komga OPDS v1.2 and v2, closes #2839 (#2851) 2026-01-03 18:04:39 +01:00
Huang Xin 8a43c58fd4 fix(tts): resolve Edge TTS being blocked in certain regions (#2741)
This should close #2739 and close #1821.
2025-12-19 03:24:51 +01:00
Huang Xin 5aa78f2554 fix(opds): expose X-Content-Length header for CORS requests (#2715) 2025-12-14 19:06:09 +01:00
Huang Xin 51008d81fb fix(opds): select proper opds search link (#2683) 2025-12-10 19:56:27 +01:00
Huang Xin 00f36af03a feat(opds): add support to search in OPDS, closes #2598 (#2634) 2025-12-06 12:13:45 +01:00
Huang Xin 75f6efe27a compat(opds): add User-Agent header to fix downloads from Calibre Web OPDS (#2592) 2025-12-02 10:27:33 +01:00
Huang Xin d2b3c165c7 fix(opds): use custom header for content length when streaming content (#2584) 2025-12-01 17:28:36 +01:00
Huang Xin 02e885fbb7 fix(opds): workaround parsing of encoded params in proxied url in cloudflare (#2575) 2025-12-01 05:49:04 +01:00
Huang Xin 2ca3561093 feat(opds): support downloading ebooks from OPDS catalogs (#2571) 2025-11-30 21:22:54 +01:00
Huang Xin cd614e3845 feat(cjk): add an option to replace quotation marks in vertical layout for CJK languages (#2511) 2025-11-22 14:09:20 +01:00
Huang Xin c86af457e7 chore: bump Next.js to 16.0.3 (#2496) 2025-11-21 12:49:36 +01:00
Huang Xin 9bc46914e2 feat(iap): support expanding cloud storage with IAP on iOS and Android (#2331) 2025-10-26 10:18:56 +01:00
Huang Xin fdf6908fc6 feat: expand cloud storage with one-time payment (#2325) 2025-10-25 11:46:51 +02:00
Huang Xin 8737535b90 feat: support Android IAP (#2286) 2025-10-21 18:24:50 +02:00
Huang Xin 686f8988bc api: use node api endpoint for iap verifying (#1683) 2025-07-26 13:34:36 +02:00
Huang Xin 42c2825e90 feat: enable IAP for upgrading to Readest Premium on iOS (#1678) 2025-07-25 19:08:53 +02:00
Huang Xin 8974a87168 feat: add IAP server api (#1676) 2025-07-25 14:44:56 +02:00
Huang Xin e3c411f043 feat: support to edit book metadata, closes #753 (#1583) 2025-07-14 19:46:57 +02:00
Huang Xin bf0bb80009 api: fix for supabase RLS (#1501) 2025-06-30 15:08:53 +02:00
Huang Xin 5d61a9808f feat: add subscription management for tauri platforms (#1499) 2025-06-30 10:04:57 +02:00
Huang Xin 280cb75ca4 api: cors in middleware (#1493) 2025-06-29 17:00:52 +02:00
Huang Xin d5d0a9abd3 api: add subscription API (#1491) 2025-06-29 14:38:04 +02:00
chrox 17336d7ac9 Add CORS for the sync API 2024-12-24 18:02:39 +01:00
Huang Xin 9be9bc8a34 Support reading progress synchronization across platforms (#34) 2024-12-24 12:31:02 +01:00