diigo-cache: the public page only reads

/public/<username> reads through the cache's viewer() facet
(PageViewer: stored pages, knownTotal, fill progress — the reader
alone), so it can't fetch from diigo or write storage.

Behavior change, agreed beforehand: an unfetched page no longer fills
itself on a miss; it says "not fetched yet" (JSON: stored: false), and
the owner fills via the webkey page. The HIT/MISS line becomes "stored
<when>". Strangers can no longer make the cache spend the owner's diigo
session; the README's deployment concern shrinks to /api.

Tests: viewing an unfetched page makes no webapp call; the page appears
once stored; fill progress shows after one fill step.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 09:26:09 -05:00
1 parent a17d287e1d
commit d7190abaf8
5 files changed
+54 -33

No files matched your search

+12 -10
View File
@@ -26,7 +26,11 @@ goal 2 preamble).
Public page n is diigo page n, filtered (may show fewer than
`PAGE_SIZE`, 100: diigo's cap).
`/public/<username>` resolves through the `idFromName("index")`
registry.
registry, and reads through `viewer()`, a read-only facet: it shows
stored pages and fill progress, and can't fetch from diigo or write.
- **Storage**: SQL lives in `src/cache-read.sql` (selects only) and
`src/cache-write.sql`; `src/store.ts` turns them into a reader and a
writer facet, so the cache logic holds no SQL text.
- **Pages**: home's dump form (cache id + page number, POST) shows raw
dump rows; `/public/<username>?page=n` shows an HTML list with a page
form (JSON for `Accept: application/json`). Both say `cache: HIT` or
@@ -72,20 +76,18 @@ show only a fingerprint (a prefix of the id's one-way hash).
Stored state is meant to be durable: see the breaking-change rule in
[capsite AGENTS.md](../AGENTS.md).
## Deployment concern: public pages spend your diigo session
## Deployment concern: rate-limit `/api`
Anyone who can reach `/public/<username>` can make the cache fetch from
diigo through *your* cookie session: each request may spend up to
`FETCH_BUDGET` (40) diigo fetches on uncached pages (goal 1d), paced at
`MS_PER_CALL`. Cached pages cost nothing. Before exposing a deployment:
Public pages only read storage, so strangers can't make the cache spend
your diigo session. `/api` is keyed (a cache id is needed), but Cap'n
Web pipelining makes calls cheap to send. Before exposing a deployment:
- rate-limit the public routes and `/api` (Cap'n Web pipelining makes
calls cheap to send) — on Cloudflare, the
- rate-limit `/api` (and `/public/*`, which reads storage) — on
Cloudflare, the
[Workers rate-limit binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)
(keyed by client and/or username); on other workerd hosts, a rate
limit in the proxy in front;
- run the fill job (goal 1e) so most pages are cached before strangers
arrive.
- run the fill job (goal 1e): public pages show only what's stored.
## RPC use (from another worker, same account)