Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Security-critical paths require a code-owner review before merge.
# These files carry the invariants documented in SECURITY.md; a subtle change here
# (loosening the Host check, binding 0.0.0.0, weakening the CSP, a default token,
# enabling jsdom scripts, or weakening .gitignore) can compromise every install.

/src/server.ts @howieyoung
/src/shared/settings.ts @howieyoung
/src/fetch/extract.ts @howieyoung
/src/archiveToken.ts @howieyoung
/src/render/archive.ts @howieyoung
/.gitignore @howieyoung
/.github/ @howieyoung
/SECURITY.md @howieyoung
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
# 只用 npm ci(鎖定 lockfile)——絕不 npm install,避免 PR 悄悄改依賴/引入惡意套件
- run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Security-invariant tests
run: npm test
- name: Security grep-gates
run: npm run security-gates
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ out/
assets/covers/
src/shared/userConfig.ts
.env
# `.env` 只精確比對,補上變體;任何位置的 SQLite 檔(含 debug 複本)都不進版控——
# 貢獻者用自己真實的瀏覽歷史跑 pipeline,一個 git add -A 誤 commit 就是永久公開外洩。
.env.*
*.db
*.db-wal
*.db-shm
*.sqlite
*.sqlite3

# Local tooling
.claude/
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ Everything runs locally; browsing data never leaves the machine.
browsing history. Never send its contents anywhere.
4. **Never send email without the user asking.** `npm run send` actually sends.
5. macOS only (Keychain, launchd, sips). On other platforms, explain the limitation honestly.
6. **The archive capability token is the one secret of the local archive server.** It MUST be
CSPRNG-only (`crypto.randomBytes`), stored in the Keychain (`browstack-archive`), and **fail
closed** when absent. Never add a hardcoded/default/derived fallback, and never mint it inside an
HTTP request handler — only in the send/rotate path. (Enforced by CI + `SECURITY.md`.)
7. **Any committed sample/screenshot/cover must come from synthetic demo data, never a real DB.**
Rendered issues and covers are built from real browsing; a "demo" artifact generated from a live
DB would permanently publish someone's actual reading week. Regenerate from synthetic data if unsure.

## Onboarding flow (walk the user through, step by step)

Expand Down
10 changes: 9 additions & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # e.g. Mondays at 09:00 (--day 0

### Números y archivo

Cada número está numerado y se conserva: №0 es el número de vista previa; a partir de ahí, cada número es simplemente №N — la progresión la lleva el propio número. Un `send` exitoso sella el número actual; la siguiente ejecución abre automáticamente uno nuevo con portada nueva. Los artefactos se acumulan en `out/` (versiones web + email por número) y `assets/covers/` (una portada por número), con un archivo navegable en `out/index.html`. Si la portada de una semana falla al renderizarse, se reutiliza la del número anterior.
Cada número está numerado y se conserva: №0 es el número de vista previa; a partir de ahí, cada número es simplemente №N — la progresión la lleva el propio número. Un `send` exitoso sella el número actual; la siguiente ejecución abre automáticamente uno nuevo con portada nueva.

**Tu lectura, como una vitrina navegable.** Cada correo semanal incluye un botón **«Abre tu archivo»**. Enlaza a una página local — servida por el mismo receptor siempre activo en `127.0.0.1:8787` — que dispone cada número anterior como un expositor de revistas, reconstruido en vivo desde tu base de datos (así incluso los números que solo recibiste por correo tienen una versión web completa). Como el archivo se sirve desde tu propia máquina:

- El botón funciona **solo en el mismo Mac, mientras el receptor esté en marcha** (instalado por `npm run schedule:weekly`). Es un enlace muerto en un teléfono o cualquier otro dispositivo — a propósito; tu lectura nunca sale de tu máquina.
- El enlace lleva un **capability token** — trátalo como una credencial de cuenta (consulta [SECURITY.md](SECURITY.md)). Puedes rotarlo cuando quieras con `npm run token:rotate` (los botones de correos antiguos dejan de funcionar; el siguiente número lleva uno nuevo).
- ¿Prefieres no rebuscar en el correo? `npm run archive:open` abre el archivo directamente en tu navegador.

Los artefactos también se acumulan en disco en `out/` (versiones web + email por número) y `assets/covers/` (una portada por número), con un índice estático en `out/index.html`. Si la portada de una semana falla al renderizarse, el correo reutiliza la del número anterior; el archivo muestra la portada propia de cada número (o la predeterminada incluida), nunca la de otro número.

## Principios editoriales

Expand Down
10 changes: 9 additions & 1 deletion README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # e.g. Mondays at 09:00 (--day 0

### Numéros et archives

Chaque numéro est numéroté et conservé : №0 est le numéro d'aperçu ; ensuite, chaque numéro est simplement №N — la progression est portée par le numéro lui-même. Un `send` réussi scelle le numéro courant ; l'exécution suivante en ouvre automatiquement un nouveau avec une couverture neuve. Les artefacts s'accumulent dans `out/` (versions web + e-mail par numéro) et `assets/covers/` (une couverture par numéro), avec des archives consultables dans `out/index.html`. Si la couverture d'une semaine échoue au rendu, celle du numéro précédent est réutilisée.
Chaque numéro est numéroté et conservé : №0 est le numéro d'aperçu ; ensuite, chaque numéro est simplement №N — la progression est portée par le numéro lui-même. Un `send` réussi scelle le numéro courant ; l'exécution suivante en ouvre automatiquement un nouveau avec une couverture neuve.

**Vos lectures, sous forme de vitrine consultable.** Chaque e-mail hebdomadaire comporte un bouton **« Ouvrir vos archives »**. Il pointe vers une page locale — servie par le même récepteur toujours actif sur `127.0.0.1:8787` — qui présente chaque numéro passé comme un présentoir à magazines, reconstruit en direct depuis votre base de données (ainsi même les numéros que vous n'avez reçus que par e-mail ont une version web complète). Comme les archives sont servies depuis votre propre machine :

- Le bouton ne fonctionne **que sur le même Mac, tant que le récepteur tourne** (installé par `npm run schedule:weekly`). C'est un lien mort sur un téléphone ou tout autre appareil — c'est voulu ; vos lectures ne quittent jamais votre machine.
- Le lien porte un **capability token** — traitez-le comme un identifiant de compte (voir [SECURITY.md](SECURITY.md)). Vous pouvez le renouveler à tout moment avec `npm run token:rotate` (les boutons des anciens e-mails cessent de fonctionner ; le numéro suivant en porte un nouveau).
- Vous préférez ne pas fouiller vos e-mails ? `npm run archive:open` ouvre les archives directement dans votre navigateur.

Les artefacts s'accumulent aussi sur le disque dans `out/` (versions web + e-mail par numéro) et `assets/covers/` (une couverture par numéro), avec un index statique dans `out/index.html`. Si la couverture d'une semaine échoue au rendu, l'e-mail réutilise celle du numéro précédent ; les archives affichent la couverture propre à chaque numéro (ou celle par défaut fournie), jamais celle d'un autre numéro.

## Principes éditoriaux

Expand Down
10 changes: 9 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # e.g. Mondays at 09:00 (--day 0

### 号数とアーカイブ

すべての号に番号が付き、保存されます:№0 はプレビュー号、それ以降の号はシンプルに №N——進行は号数そのものが伝えます。`send` の成功が現在の号を封緘し、次の実行は自動的に新しい号を新しい表紙で開きます。成果物は `out/`(号ごとの Web 版+メール版)と `assets/covers/`(号ごとに 1 枚の表紙)に蓄積され、`out/index.html` で閲覧可能なアーカイブになります。ある週の表紙レンダリングが失敗しても、前号の表紙が再利用されます。
すべての号に番号が付き、保存されます:№0 はプレビュー号、それ以降の号はシンプルに №N——進行は号数そのものが伝えます。`send` の成功が現在の号を封緘し、次の実行は自動的に新しい号を新しい表紙で開きます。

**あなたの読書を、閲覧できるショーケースに。** 毎週のメールには **「アーカイブを開く」** ボタンが入っています。これは `127.0.0.1:8787` で常駐する同じ受信サービスが配信するローカルページへのリンクで、過去のすべての号を雑誌ラックのように並べ、あなたのデータベースからライブで再構築します(メールでしか受け取っていない号にも完全な Web 版が用意されます)。アーカイブはあなた自身のマシンから配信されるため:

- このボタンは**同じ Mac 上で、受信サービスが動作している間だけ**機能します(`npm run schedule:weekly` でインストール)。スマートフォンやその他のデバイスではリンク切れになります——これは意図的な設計です。あなたの読書はマシンの外に出ません。
- リンクには **capability token** が含まれます——アカウントの認証情報と同じように扱ってください([SECURITY.md](SECURITY.md) を参照)。`npm run token:rotate` でいつでもローテーションできます(古いメールのボタンは無効になり、次の号が新しいものを持ちます)。
- メールを探すのが面倒?`npm run archive:open` でブラウザからアーカイブを直接開けます。

成果物はディスク上にも蓄積されます:`out/`(号ごとの Web 版+メール版)、`assets/covers/`(号ごとに 1 枚の表紙)、そして静的インデックス `out/index.html`。ある週の表紙レンダリングが失敗した場合、メールは前号の表紙を再利用します。アーカイブは各号自身の表紙(またはバンドルされたデフォルト)を表示し、他号の表紙を使うことはありません。

## 編集原則

Expand Down
10 changes: 9 additions & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # e.g. Mondays at 09:00 (--day 0

### 호수와 아카이브

모든 호에 번호가 붙고 보존됩니다: №0은 프리뷰호, 그 이후의 모든 호는 간단히 №N——진행은 호수 자체가 전달합니다. `send` 성공이 현재 호를 봉인하고, 다음 실행은 자동으로 새 표지와 함께 새 호를 엽니다. 결과물은 `out/`(호별 웹 + 이메일 버전)과 `assets/covers/`(호당 표지 1장)에 쌓이며, `out/index.html`에서 아카이브를 열람할 수 있습니다. 어느 주의 표지 렌더링이 실패해도 이전 호의 표지가 재사용됩니다.
모든 호에 번호가 붙고 보존됩니다: №0은 프리뷰호, 그 이후의 모든 호는 간단히 №N——진행은 호수 자체가 전달합니다. `send` 성공이 현재 호를 봉인하고, 다음 실행은 자동으로 새 표지와 함께 새 호를 엽니다.

**당신의 읽기를 열람 가능한 쇼케이스로.** 매주 이메일에는 **"아카이브 열기"** 버튼이 들어 있습니다. `127.0.0.1:8787`에서 상주하는 동일한 수신 서비스가 제공하는 로컬 페이지로 연결되며, 지난 모든 호를 잡지 진열대처럼 배치하고 데이터베이스에서 실시간으로 다시 만듭니다(이메일로만 받은 호에도 완전한 웹 버전이 생깁니다). 아카이브는 당신 자신의 기기에서 제공되므로:

- 이 버튼은 **같은 Mac에서, 수신 서비스가 실행 중일 때만** 작동합니다(`npm run schedule:weekly`로 설치). 휴대폰이나 다른 기기에서는 죽은 링크입니다——의도된 설계이며, 당신의 읽기는 기기를 떠나지 않습니다.
- 링크에는 **capability token**이 담깁니다——계정 자격 증명처럼 다루세요([SECURITY.md](SECURITY.md) 참고). `npm run token:rotate`로 언제든 교체할 수 있습니다(기존 이메일의 버튼은 무효화되고, 다음 호가 새 링크를 가집니다).
- 이메일을 뒤지기 번거롭다면? `npm run archive:open`으로 브라우저에서 아카이브를 바로 엽니다.

결과물은 디스크에도 쌓입니다: `out/`(호별 웹 + 이메일 버전)과 `assets/covers/`(호당 표지 1장), 그리고 정적 인덱스 `out/index.html`. 어느 주의 표지 렌더링이 실패하면 이메일은 이전 호의 표지를 재사용하고, 아카이브는 각 호 자신의 표지(또는 번들 기본 표지)를 보여주며 다른 호의 표지를 쓰지 않습니다.

## 편집 원칙

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # e.g. Mondays at 09:00 (--day 0

### Issues & archive

Every issue is numbered and kept: №0 is the preview issue; every issue after it is simply №N — progression is carried by the number itself. A successful `send` seals the current issue; the next run automatically opens a new one with a fresh cover. Artifacts accumulate under `out/` (web + email versions per issue) and `assets/covers/` (one cover per issue), with a browsable archive at `out/index.html`. If a week's cover fails to render, the previous issue's cover is reused.
Every issue is numbered and kept: №0 is the preview issue; every issue after it is simply №N — progression is carried by the number itself. A successful `send` seals the current issue; the next run automatically opens a new one with a fresh cover.

**Your reading, as a browsable showcase.** Every weekly email carries an **"Open your archive"** button. It links to a local page — served by the same always-on receiver on `127.0.0.1:8787` — that lays out every past issue like a magazine rack, rebuilt live from your database (so even issues you only ever received by email get a full web view). Because the archive is served from your own machine:

- The button works **only on the same Mac, while the receiver is running** (installed by `npm run schedule:weekly`). It is a dead link on a phone or any other device — by design; your reading never leaves your machine.
- The link carries a **capability token** — treat it like an account credential (see [SECURITY.md](SECURITY.md)). Rotate it any time with `npm run token:rotate` (old email buttons stop working; the next issue carries a fresh one).
- Prefer not to dig through email? `npm run archive:open` opens the archive in your browser directly.

Artifacts also accumulate on disk under `out/` (web + email versions per issue) and `assets/covers/` (one cover per issue), with a static index at `out/index.html`. If a week's cover fails to render, the email reuses the previous issue's cover; the archive shows each issue's own cover (or the bundled default), never another issue's art.

## Editorial principles

Expand Down
10 changes: 9 additions & 1 deletion README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,15 @@ npm run schedule:weekly -- --day 1 --hour 9 # 例:每週一 09:00(--day 0

### 期數與典藏

每一期都有編號並永久留存:№0 是創刊預覽號,之後每一期以編號 №N 呈現——進展由期數本身傳達。`send` 成功即封刊,下一次產出自動開新的一期並生成新封面。刊物累積在 `out/`(每期的網頁版+email 版)、封面累積在 `assets/covers/`(每期一張),並有可瀏覽的典藏索引 `out/index.html`。某週封面渲染失敗時,沿用上一期封面、不擋出刊。
每一期都有編號並永久留存:№0 是創刊預覽號,之後每一期以編號 №N 呈現——進展由期數本身傳達。`send` 成功即封刊,下一次產出自動開新的一期並生成新封面。

**你的閱讀,一座可瀏覽的櫥窗。** 每封每週刊物信裡都有一個 **「開啟你的典藏」** 按鈕,連到一個本機頁面——由那個常駐於 `127.0.0.1:8787` 的接收服務端出——像刊物櫥窗一樣陳列歷來每一期,並即時從你的資料庫重建(所以連只用 email 收過的期數也有完整網頁版)。因為典藏由你自己的機器端出:

- 這個按鈕只在**同一台 Mac、且接收服務運行時**有效(由 `npm run schedule:weekly` 安裝)。在手機或任何其他裝置上都是死連結——這是刻意的設計;你的閱讀永不離開這台機器。
- 連結帶有一個 **capability token**——請把它當成帳號憑證看待(見 [SECURITY.md](SECURITY.md))。隨時可用 `npm run token:rotate` 更新(舊信件的按鈕即刻失效,下一期會帶新的)。
- 不想翻信件?`npm run archive:open` 直接在瀏覽器開啟典藏。

刊物同時也累積在磁碟上:`out/`(每期的網頁版+email 版)、`assets/covers/`(每期一張封面),並有靜態索引 `out/index.html`。某週封面渲染失敗時,email 沿用上一期封面;典藏則顯示各期自己的封面(或隨庫預設封面),絕不張冠李戴用到別期的插畫。

## 編輯原則

Expand Down
Loading
Loading