chore(send-to-readest): v0.2.1, zip packaging script, store submission doc (#4661)

Prep for the Chrome Web Store submission of the Send to Readest browser
extension:

- Bump manifest + package version 0.2.0 -> 0.2.1.
- Add a `pnpm zip` script (scripts/zip.mjs) that builds and packages dist/
  into a versioned, Web-Store-ready archive: manifest.json at the zip root,
  excluding webpack's `.LICENSE.txt` banners and `.DS_Store`. Ignore the
  produced `*.zip` in the extension's .gitignore.
- Add STORE-SUBMISSION.md: copy-paste dashboard answers (single purpose,
  per-permission justifications, remote-code answer, data-use disclosures,
  listing copy) pointing at the canonical privacy policy hosted at
  https://www.readest.com/send-to-readest/privacy-policy.

Verified: pnpm test:extension (51 passed), pnpm build-browser-ext, pnpm zip
(valid archive), pnpm lint clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-19 16:30:30 +08:00
committed by GitHub
parent 1faa931a0e
commit 4cb608be20
5 changed files with 162 additions and 2 deletions
@@ -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/
@@ -0,0 +1,117 @@
# Chrome Web Store — submission copy
Copy-paste answers for the Developer Dashboard. Edit `<FILL IN>` 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 (`<all_urls>`)**
> 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.
@@ -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"],
@@ -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"
},
@@ -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`);