diff --git a/apps/readest-app/extensions/send-to-readest/.gitignore b/apps/readest-app/extensions/send-to-readest/.gitignore index f0183443..ae9c88d4 100644 --- a/apps/readest-app/extensions/send-to-readest/.gitignore +++ b/apps/readest-app/extensions/send-to-readest/.gitignore @@ -3,6 +3,9 @@ # `chrome://extensions`. dist/ +# Versioned upload archives produced by `pnpm zip`. +*.zip + # Per-package node_modules (the workspace hoists most of it to the root, # but pnpm still drops a `.pnpm` symlink tree here). node_modules/ diff --git a/apps/readest-app/extensions/send-to-readest/STORE-SUBMISSION.md b/apps/readest-app/extensions/send-to-readest/STORE-SUBMISSION.md new file mode 100644 index 00000000..e6e6c7f2 --- /dev/null +++ b/apps/readest-app/extensions/send-to-readest/STORE-SUBMISSION.md @@ -0,0 +1,117 @@ +# Chrome Web Store — submission copy + +Copy-paste answers for the Developer Dashboard. Edit `` placeholders +before submitting. Keep these in sync with `manifest.json` — if a permission is +added or removed, update the matching justification here. + +--- + +## Single purpose + +> Send to Readest saves the web page you are currently viewing into your +> personal Readest library as a self-contained EPUB. When you click the +> toolbar button, it extracts the readable article and its images and uploads +> the result to your own Readest account. + +## Permission justifications + +Paste each into the **Privacy practices → Permission justification** field. + +**`activeTab`** +> Lets the extension read the page in the active tab at the moment the user +> clicks the toolbar button, so it can capture the article the user is +> currently viewing. No access is taken until the user invokes the action. + +**`scripting`** +> Used to inject the capture script into the active tab on click, which reads +> the page's rendered DOM (via Mozilla Readability) to extract the article +> body and its image references. It is not used to run any remotely hosted +> code — the script is bundled in the extension. + +**`storage`** +> Caches the user's own Readest sign-in token (synced from the Readest website) +> and an optional custom server URL in local extension storage, so the upload +> can be authenticated without re-prompting for credentials. No browsing data +> is stored. + +**`offscreen`** +> Creates an offscreen document to assemble the EPUB file, because the EPUB +> building step needs DOM/Blob APIs that are unavailable in the service worker. +> The offscreen document does no network or user-facing work. + +**Host permissions (``)** +> The user can clip any web page, so the extension needs access to arbitrary +> sites for two things, both triggered only by an explicit click: +> (1) injecting the capture script into the page being clipped, and +> (2) downloading the images that page references — using the user's existing +> session so images behind a login or paywall capture correctly. Content and +> images are sent only to the user's Readest account; nothing is sent to any +> third party. + +**Content scripts on `*.readest.com` / `localhost:3000`** +> A content script runs only on Readest's own sites to copy the user's existing +> Readest access token into the extension so uploads are authenticated to the +> user's account. It never reads a password or refresh token. + +## Remote code + +> **No.** All libraries (zip.js, Mozilla Readability, DOMPurify, language +> detection) are bundled in the extension package and execute locally. The +> extension downloads only the images of the page the user chose to clip and +> uploads the resulting EPUB to the user's Readest account. + +## Data use disclosures + +**Data types collected** (check in the dashboard): +- ☑ **Website content** — the page text and images the user clips. +- ☑ **Authentication information** — the user's Readest access token, used + solely to authenticate the upload to the user's own account. +- ☐ Everything else (personally identifiable info, location, financial, + health, personal communications, web history, user activity) — **not** + collected. + +**Certifications** (all three apply — check each): +- ☑ I do not sell or transfer user data to third parties, outside of the + approved use cases. +- ☑ I do not use or transfer user data for purposes unrelated to my item's + single purpose. +- ☑ I do not use or transfer user data to determine creditworthiness or for + lending purposes. + +**Privacy policy URL:** `https://www.readest.com/send-to-readest/privacy-policy` +(canonical; maintained in the `readest-landing` repo at +`src/app/[locale]/(legal)/send-to-readest/privacy-policy/page.mdx`) + +## Store listing copy + +**Name:** Send to Readest + +**Short description (132 char max):** +> Save web pages to your Readest library + +**Detailed description (suggested):** +> Send to Readest saves the page you're reading straight to your Readest +> library with one click — as a clean, self-contained EPUB you can read on any +> device. +> +> • One click captures the readable article, stripped of clutter. +> • Images are bundled in, so the saved page opens fully offline. +> • Works on pages behind a login or paywall — it captures what you can +> actually see, using your existing session. +> • The clipped book syncs to Readest on all your devices. +> +> You stay in control: nothing is captured until you click the button, and your +> clips are sent only to your own Readest account. Sign in to Readest once and +> you're set. + +**Category:** Productivity + +**Language:** English (en) + +## Assets checklist (still needed before submitting) + +- [ ] At least one screenshot, 1280×800 or 640×400 (PNG/JPG) — e.g. the popup + mid-clip on a real article. +- [ ] Store icon 128×128 — present at `icons/icon-128.png`. +- [ ] Privacy policy hosted, URL filled in above. +- [ ] (Optional) Promo tile 440×280 for featured placement. diff --git a/apps/readest-app/extensions/send-to-readest/manifest.json b/apps/readest-app/extensions/send-to-readest/manifest.json index 164519e9..2fc685c4 100644 --- a/apps/readest-app/extensions/send-to-readest/manifest.json +++ b/apps/readest-app/extensions/send-to-readest/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_app_name__", - "version": "0.2.0", + "version": "0.2.1", "description": "__MSG_app_description__", "default_locale": "en", "permissions": ["activeTab", "scripting", "storage", "offscreen"], diff --git a/apps/readest-app/extensions/send-to-readest/package.json b/apps/readest-app/extensions/send-to-readest/package.json index 7684bcdc..91caeff9 100644 --- a/apps/readest-app/extensions/send-to-readest/package.json +++ b/apps/readest-app/extensions/send-to-readest/package.json @@ -1,12 +1,13 @@ { "name": "@readest/send-to-readest-extension", - "version": "0.2.0", + "version": "0.2.1", "private": true, "description": "Browser extension that clips the current web page into the user's Readest library as a self-contained EPUB.", "scripts": { "build": "webpack --mode production", "dev": "webpack --watch --mode development", "clean": "rimraf dist", + "zip": "pnpm build && node scripts/zip.mjs", "i18n:extract": "node scripts/extract-i18n.mjs", "i18n:check": "node scripts/extract-i18n.mjs --check" }, diff --git a/apps/readest-app/extensions/send-to-readest/scripts/zip.mjs b/apps/readest-app/extensions/send-to-readest/scripts/zip.mjs new file mode 100644 index 00000000..7f11d660 --- /dev/null +++ b/apps/readest-app/extensions/send-to-readest/scripts/zip.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +/** + * Package the built extension into a versioned, Chrome-Web-Store-ready zip. + * + * Invoked by `pnpm zip`, which builds first so the archive is never stale. + * The zip root holds `manifest.json` directly — the layout the Web Store + * dashboard expects — and drops the `.LICENSE.txt` banners webpack emits + * plus macOS `.DS_Store` noise. + * + * Requires the `zip` CLI (present on macOS / Linux). This is a maintainer + * packaging step, not part of the CI build. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +const DIST = join(ROOT, 'dist'); +const MANIFEST = join(DIST, 'manifest.json'); + +if (!existsSync(MANIFEST)) { + console.error('[zip] dist/manifest.json not found — run `pnpm build` first.'); + process.exit(1); +} + +const { version } = JSON.parse(readFileSync(MANIFEST, 'utf8')); +const out = join(ROOT, `send-to-readest-${version}.zip`); +if (existsSync(out)) rmSync(out); + +// Zip the *contents* of dist/ (cwd: DIST) so manifest.json lands at the +// archive root rather than under a `dist/` prefix. +execFileSync('zip', ['-rq', out, '.', '-x', '*.DS_Store', '*.LICENSE.txt'], { + cwd: DIST, + stdio: 'inherit', +}); + +const kb = Math.round(statSync(out).size / 1024); +console.log(`[zip] send-to-readest-${version}.zip (${kb} KB) ready to upload`);