* 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>
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>
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>
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>
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>
* 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>