Initial build: stash server + browser extension

Server (Express + better-sqlite3 + FTS5, mirroring keep's stack):
/api/capture (upsert-on-url, per PROPOSAL.md's dedup decision),
/api/articles (list/search/mark-read/delete), plus a minimal
server-rendered reading list and reader view at / and /read/:id
(token passed as ?t= there since browser navigation can't set an
Authorization header). Bearer-token auth generated via `npm run
token` — no login flow, matching keep's one-time identity
registration pattern.

Extension (Manifest V3, sideloaded, no store distribution): toolbar
click or context menu "Send to stash" runs @mozilla/readability
(vendored into extension/lib/) against the current page, falling back
to raw rendered HTML when extraction comes back too thin. Options
page for one-time server URL + token entry.

Verified end-to-end with the real extension loaded in a live Chromium
instance (not just unit-tested extraction logic): capture against a
real page, dedup-on-recapture, mark-read, FTS5 search, 401 on missing
auth, and both server-rendered views all confirmed working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-12 23:23:01 +02:00
commit f2ebfe35d4
19 changed files with 5449 additions and 0 deletions

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
# stash
A self-hosted read-later tool. Click "Send to stash" in the browser, the
current page's article content is extracted and stored on your own
server — full-text searchable, with a plain reader view. No third party
ever sees your reading list.
Two pieces, neither useful alone:
- **Server** (`src/server`) — Express + SQLite (via `better-sqlite3`),
FTS5 full-text search, a minimal server-rendered reading list + reader
view, and the `/api/*` endpoints the extension talks to.
- **Extension** (`extension/`) — Manifest V3, sideloaded (not published to
any store). Runs [`@mozilla/readability`](https://github.com/mozilla/readability)
against the current page to strip nav/ads/chrome and keep just the
article, falling back to the raw rendered page if extraction comes back
too thin to be useful.
Full design and the reasoning behind the scope boundaries: [PROPOSAL.md](./PROPOSAL.md).
## Server setup
```bash
cp .env.example .env
npm install
npm run token -- my-laptop # prints a bearer token — paste it into the extension's options
npm run dev:server
```
## Extension setup
1. `chrome://extensions` → enable Developer mode → "Load unpacked" → select `extension/`
2. Click the extension icon once, or use "Options" from its context menu, to open the options page
3. Enter your server's URL and the token from `npm run token`
Then either click the toolbar icon or right-click a page → "Send to stash".
## Reading
Visit `http://your-server:3070/?t=<token>` — a plain list of what's been
captured, with search and an unread filter. Click through to `/read/:id`
for the cleaned reader view.
## Status
Built and verified end-to-end: capture → server storage → full-text
search → mark-read → reader view, driven through the actual extension
(loaded in a real Chromium instance, not just unit-tested logic) against
a live server. Not yet deployed anywhere.
## License
MIT.