fixup! diigo-cache: the public page only reads

Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 09:34:06 -05:00
1 parent 2a1942fc2a
commit 164d3cb8cb
2 files changed
+62 -81

No files matched your search

+13 -15
View File
@@ -1,14 +1,6 @@
/**
* @file the cache Durable Objects' logic: `DiigoCacheLogic` (public+private
* items) and `PublicCacheLogic` (public-only items, widely shared). Logic
* only: each requires its {@link Clock}; the worker's entry module
* (src/index.ts) is the edge that makes the live one and exports the
* runtime-constructed `DiigoCache`/`PublicCache`. They are independent
* and work the same way ({@link PagedCache}): pages are the unit of
* caching; a hit is answered from `ctx.storage.sqlite` with no diigo call;
* a miss asks the webapp instance in the cache's wiring and etches the
* page. The only difference is what each keeps: the public cache drops
* private rows *before* etching ({@link publicOnly}).
* @file the cache Durable Objects' logic: a private and a public page cache.
* The runtime classes are made at the edge (src/index.ts).
*/
import { DurableObject, RpcTarget } from "cloudflare:workers";
@@ -112,11 +104,17 @@ export type CacheEnv = {
};
/**
* the shared page-cache behavior. The webapp-instance id in the wiring
* authorizes diigo reads (the cookie rides in that DO's storage); the id
* this DO is addressed by authorizes cache reads. Neither is logged.
* `setup()` persists the wiring (webapp's configure pattern); `page(n)` is
* the read path; subclasses choose what to `keep` of a fetched page.
* the shared page-cache behavior; {@link DiigoCacheLogic} and
* {@link PublicCacheLogic} are independent and differ only in what they
* `keep` of a fetched page (the public one drops private rows before
* storing, {@link publicOnly}). Pages are the unit of caching: a hit is
* served from storage with no diigo call; a miss asks the webapp instance
* in the cache's wiring and stores the page.
*
* The webapp id in the wiring authorizes diigo reads (the cookie rides in
* that DO's storage); the id this DO is addressed by authorizes cache
* reads. Neither is logged. The {@link Clock} is required: time comes from
* the edge that constructs the DO.
*/
abstract class PagedCache extends DurableObject<CacheEnv> {
#step = 0;
+49 -66
View File
@@ -1,15 +1,6 @@
/**
* @file diigo-cache worker: caches diigo bookmark dumps in Durable-Object
* SQLite storage. `PrivateCache` (public+private items) is addressed by its
* own unique id — minted by the create form alongside the webapp instance
* (the cookie lives in that DO's storage); `PublicCache` (public-only
* items) is widely shared, addressed by the diigo username in the path.
* Dump output stays raw; a repeat page load is answered from storage with
* no diigo call.
*
* The create form's ids reach the browser only via explicit user action
* (clipboard copy / file save — the poor-man's powerbox); a pasted id is
* how the home page's dump POST knows which cache to hit.
* @file diigo-cache worker: the create flow, the webkey page and its API,
* and public pages. See the default export for its routes.
*/
import { WorkerEntrypoint } from "cloudflare:workers";
@@ -34,18 +25,18 @@ function liveClock(): Clock {
return { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
}
// The DO classes must be exports of the main module, and the runtime
// constructs them with (ctx, env) only: a Cloudflare API constraint, met
// here at the edge by subclasses that supply the live clock.
/** the private cache, as the runtime constructs it */
/**
* the private cache, as the runtime constructs it. DO classes must be
* exports of the main module, and the runtime passes only (ctx, env): a
* Cloudflare constraint, met here at the edge by supplying the live clock.
*/
export class DiigoCache extends DiigoCacheLogic {
constructor(ctx: DurableObjectState, env: CacheEnv) {
super(ctx, env, liveClock());
}
}
/** the public cache, as the runtime constructs it */
/** the public cache, as the runtime constructs it (see {@link DiigoCache}) */
export class PublicCache extends PublicCacheLogic {
constructor(ctx: DurableObjectState, env: CacheEnv) {
super(ctx, env, liveClock());
@@ -84,77 +75,66 @@ export class DiigoBridge extends WorkerEntrypoint {
}
}
/**
* the worker's routes:
*
* - `GET /`: home ({@link home}); `POST /`: dump a page by cache id ({@link dump})
* - `GET /_connect`: webapp's mint form, relayed ({@link mintPage});
* `POST /_mint`: the mint ({@link proxyMint})
* - `POST /_wire`: wire a cache to a webapp id ({@link wire});
* `POST /_publish`: add a public companion later ({@link publishCache})
* - `GET /cache/`: the webkey page ({@link CACHE_PAGE_HTML});
* `/api`: its Cap'n Web API ({@link CacheApi}); `GET /_capnweb.js`
* - `GET /public/<username>`: public pages, read-only ({@link publicPage})
*
* Cache ids are webkeys (docs/webkeys.md): they ride POST bodies and URL
* fragments, never query strings or paths.
*/
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// public cache: widely shared, username in the path
const pub = url.pathname.match(/^\/public\/([A-Za-z0-9_-]+)$/);
if (pub) return publicPage(request, env, pub[1]);
// webapp's mint form, relayed onto our port (wrangler multi-config
// dev doesn't mount an aux HTTP server): diigo-cache supplies only
// the diigo prefill (base URL + UA headers) and the form HTML is
// webapp's own — its init params (url, headers, msPerCall) are
// editable there; the POST relays opaquely (a conduit: the cookie
// crosses, no code on this side opens/logs it).
if (url.pathname === "/_connect" && request.method === "GET") {
return mintPage(env);
}
// the mint proxied: form fields (incl. the cookie) relay to
// WEBAPP.newWebApp; the response is the wire form, webapp id filled
if (url.pathname === "/_mint" && request.method === "POST") {
return proxyMint(request, env);
}
// the wire step: mint sent us back the webapp id (form-encoded from
// the mint form's hidden-continue, or JSON service-style). No cookie
// ever arrives here.
if (url.pathname === "/_wire" && request.method === "POST") {
return wire(request, env);
}
// publish a cache that was wired private-only: adds the public
// companion (derived from the private cache's stored wiring — the
// webapp id rides along, no re-mint), registers the username read
// from its items, seeds the projection. The cache id is a webkey: it
// rides the POST body, never the path.
if (url.pathname === "/_publish" && request.method === "POST") {
return publishCache(request, env);
}
// a cache's webkey page: the key rides the fragment (never sent
// here); the page trades it for a capability over /api
if (url.pathname === "/cache/" && request.method === "GET") {
return new Response(CACHE_PAGE_HTML, { headers: { "content-type": "text/html;charset=utf-8" } });
}
if (url.pathname === "/_capnweb.js" && request.method === "GET") {
return new Response(CAPNWEB_BROWSER_JS, { headers: { "content-type": "text/javascript;charset=utf-8" } });
}
// Cap'n Web: in-band authorization (CacheApi.cache(key))
if (url.pathname === "/api") {
return newWorkersRpcResponse(request, new CacheApi(env));
}
if (url.pathname !== "/") return new Response("not found", { status: 404 });
if (request.method === "POST") {
return dump(request, env);
}
// GET / is inert (dumps happen on POST submits). The cache id is a
// webkey: it never rides a query string or path (they leak into
// history/Referer/logs — see docs/webkeys.md); the form POST is how
// it travels here.
// GET is inert: a cache id never rides a query string
return home(env, { error: "GET is inert: dumps happen on POST submits" }, null);
},
};
/** the connect page: webapp's own mint form HTML (fetched over the
* service binding, action rewritten to our relay), carried with diigo's
* prefill only — the editable init params are webapp's, diigo-cache
* authors no form fields of its own */
/**
* the connect page: webapp's own mint form HTML, fetched over the service
* binding with diigo's prefill (base URL, UA headers) and its action
* rewritten to our relay. Served here because wrangler's multi-config dev
* doesn't serve an aux worker's HTTP. diigo-cache authors no fields of its
* own; the init params stay editable, webapp's.
*/
async function mintPage(env: Env): Promise<Response> {
const prefill =
`?render=1&title=${encodeURIComponent("diigo cache: mint the session")}` +
@@ -252,12 +232,13 @@ async function wirePage(webappId: string): Promise<Response> {
});
}
/** the wire step: webapp-instance id + the public/private choice. The
* cache is minted, wired to {webappId, publicId}, and page 1 is etched;
* in public mode the diigo username is read from the fetched items
* (self-authenticating: the cookie's account, never form input) and the
* public cache is wired to the same webapp id and registered. The two
* caches are independent from then on: each fills itself on a miss. */
/**
* the wire step: a webapp id plus the public/private choice. The cache is
* minted, wired to {webappId, publicId}, and page 1 is stored. In public
* mode the diigo username is read from the fetched items (the cookie's
* own account, never form input), and the public cache is wired to the
* same webapp id and registered under it. No cookie arrives here.
*/
async function wire(request: Request, env: Env): Promise<Response> {
const ct = request.headers.get("content-type") ?? "";
let webappId = "";
@@ -369,9 +350,11 @@ function fillLine(fill: FillStatus): string {
return `<p>${esc(said)}</p>`;
}
/** publish, later: the private cache's wiring already holds the webapp
* id, so the public companion needs no re-mint; the username comes from
* the items already in the cache */
/**
* publish, later: adds a public companion to a cache wired private-only.
* Its wiring already holds the webapp id, so no re-mint; the username
* comes from the items already cached. The cache id rides the POST body.
*/
async function publishCache(request: Request, env: Env): Promise<Response> {
let cacheId = "";
try {
@@ -465,10 +448,12 @@ async function dump(request: Request, env: Env): Promise<Response> {
const PUBLIC_PER_PAGE = PAGE_SIZE;
/** the public page for a username: an HTML page with a page-number form
* (GET — nothing secret here, so `?page=` in the URL is fine, unlike the
* webkey routes), or the same page as JSON for `Accept: application/json`
* callers */
/**
* the public page for a username: stored pages only, through the cache's
* read-only `viewer()` (it can't fetch or write). HTML with a page-number
* form, or JSON for `Accept: application/json`. `?page=` in the URL is
* fine here: nothing on this page is secret.
*/
async function publicPage(request: Request, env: Env, username: string): Promise<Response> {
const index = env.PUBLIC_DO.get(env.PUBLIC_DO.idFromName("index"));
const publicId = await index.lookup(username);
@@ -479,8 +464,6 @@ async function publicPage(request: Request, env: Env, username: string): Promise
const raw = new URL(request.url).searchParams.get("page");
const pageN = raw === null ? 1 : Number.parseInt(raw, 10);
const valid = !Number.isNaN(pageN) && pageN >= 1 && pageN <= 10000;
// read-only: the viewer facet serves stored pages and cannot fetch
// from diigo or write storage; filling is the owner's (webkey page)
const viewer = await cache.viewer();
let page, total, fill;
try {