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 * @file the cache Durable Objects' logic: a private and a public page cache.
* items) and `PublicCacheLogic` (public-only items, widely shared). Logic * The runtime classes are made at the edge (src/index.ts).
* 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}).
*/ */
import { DurableObject, RpcTarget } from "cloudflare:workers"; 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 * the shared page-cache behavior; {@link DiigoCacheLogic} and
* authorizes diigo reads (the cookie rides in that DO's storage); the id * {@link PublicCacheLogic} are independent and differ only in what they
* this DO is addressed by authorizes cache reads. Neither is logged. * `keep` of a fetched page (the public one drops private rows before
* `setup()` persists the wiring (webapp's configure pattern); `page(n)` is * storing, {@link publicOnly}). Pages are the unit of caching: a hit is
* the read path; subclasses choose what to `keep` of a fetched page. * 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> { abstract class PagedCache extends DurableObject<CacheEnv> {
#step = 0; #step = 0;
+49 -66
View File
@@ -1,15 +1,6 @@
/** /**
* @file diigo-cache worker: caches diigo bookmark dumps in Durable-Object * @file diigo-cache worker: the create flow, the webkey page and its API,
* SQLite storage. `PrivateCache` (public+private items) is addressed by its * and public pages. See the default export for its routes.
* 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.
*/ */
import { WorkerEntrypoint } from "cloudflare:workers"; import { WorkerEntrypoint } from "cloudflare:workers";
@@ -34,18 +25,18 @@ function liveClock(): Clock {
return { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }; 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 * the private cache, as the runtime constructs it. DO classes must be
// here at the edge by subclasses that supply the live clock. * 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.
/** the private cache, as the runtime constructs it */ */
export class DiigoCache extends DiigoCacheLogic { export class DiigoCache extends DiigoCacheLogic {
constructor(ctx: DurableObjectState, env: CacheEnv) { constructor(ctx: DurableObjectState, env: CacheEnv) {
super(ctx, env, liveClock()); 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 { export class PublicCache extends PublicCacheLogic {
constructor(ctx: DurableObjectState, env: CacheEnv) { constructor(ctx: DurableObjectState, env: CacheEnv) {
super(ctx, env, liveClock()); 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 { export default {
async fetch(request: Request, env: Env): Promise<Response> { async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
// public cache: widely shared, username in the path
const pub = url.pathname.match(/^\/public\/([A-Za-z0-9_-]+)$/); const pub = url.pathname.match(/^\/public\/([A-Za-z0-9_-]+)$/);
if (pub) return publicPage(request, env, pub[1]); 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") { if (url.pathname === "/_connect" && request.method === "GET") {
return mintPage(env); 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") { if (url.pathname === "/_mint" && request.method === "POST") {
return proxyMint(request, env); 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") { if (url.pathname === "/_wire" && request.method === "POST") {
return wire(request, env); 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") { if (url.pathname === "/_publish" && request.method === "POST") {
return publishCache(request, env); 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") { if (url.pathname === "/cache/" && request.method === "GET") {
return new Response(CACHE_PAGE_HTML, { headers: { "content-type": "text/html;charset=utf-8" } }); return new Response(CACHE_PAGE_HTML, { headers: { "content-type": "text/html;charset=utf-8" } });
} }
if (url.pathname === "/_capnweb.js" && request.method === "GET") { if (url.pathname === "/_capnweb.js" && request.method === "GET") {
return new Response(CAPNWEB_BROWSER_JS, { headers: { "content-type": "text/javascript;charset=utf-8" } }); 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") { if (url.pathname === "/api") {
return newWorkersRpcResponse(request, new CacheApi(env)); return newWorkersRpcResponse(request, new CacheApi(env));
} }
if (url.pathname !== "/") return new Response("not found", { status: 404 }); if (url.pathname !== "/") return new Response("not found", { status: 404 });
if (request.method === "POST") { if (request.method === "POST") {
return dump(request, env); return dump(request, env);
} }
// GET is inert: a cache id never rides a query string
// 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.
return home(env, { error: "GET is inert: dumps happen on POST submits" }, null); 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 * the connect page: webapp's own mint form HTML, fetched over the service
* prefill only — the editable init params are webapp's, diigo-cache * binding with diigo's prefill (base URL, UA headers) and its action
* authors no form fields of its own */ * 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> { async function mintPage(env: Env): Promise<Response> {
const prefill = const prefill =
`?render=1&title=${encodeURIComponent("diigo cache: mint the session")}` + `?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; * the wire step: a webapp id plus the public/private choice. The cache is
* in public mode the diigo username is read from the fetched items * minted, wired to {webappId, publicId}, and page 1 is stored. In public
* (self-authenticating: the cookie's account, never form input) and the * mode the diigo username is read from the fetched items (the cookie's
* public cache is wired to the same webapp id and registered. The two * own account, never form input), and the public cache is wired to the
* caches are independent from then on: each fills itself on a miss. */ * same webapp id and registered under it. No cookie arrives here.
*/
async function wire(request: Request, env: Env): Promise<Response> { async function wire(request: Request, env: Env): Promise<Response> {
const ct = request.headers.get("content-type") ?? ""; const ct = request.headers.get("content-type") ?? "";
let webappId = ""; let webappId = "";
@@ -369,9 +350,11 @@ function fillLine(fill: FillStatus): string {
return `<p>${esc(said)}</p>`; 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 * publish, later: adds a public companion to a cache wired private-only.
* the items already in the cache */ * 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> { async function publishCache(request: Request, env: Env): Promise<Response> {
let cacheId = ""; let cacheId = "";
try { try {
@@ -465,10 +448,12 @@ async function dump(request: Request, env: Env): Promise<Response> {
const PUBLIC_PER_PAGE = PAGE_SIZE; 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 * the public page for a username: stored pages only, through the cache's
* webkey routes), or the same page as JSON for `Accept: application/json` * read-only `viewer()` (it can't fetch or write). HTML with a page-number
* callers */ * 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> { async function publicPage(request: Request, env: Env, username: string): Promise<Response> {
const index = env.PUBLIC_DO.get(env.PUBLIC_DO.idFromName("index")); const index = env.PUBLIC_DO.get(env.PUBLIC_DO.idFromName("index"));
const publicId = await index.lookup(username); 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 raw = new URL(request.url).searchParams.get("page");
const pageN = raw === null ? 1 : Number.parseInt(raw, 10); const pageN = raw === null ? 1 : Number.parseInt(raw, 10);
const valid = !Number.isNaN(pageN) && pageN >= 1 && pageN <= 10000; 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(); const viewer = await cache.viewer();
let page, total, fill; let page, total, fill;
try { try {