From 164d3cb8cba1b08616186983f854268af58d5f92 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 7 Oct 2026 09:34:06 -0500 Subject: [PATCH] fixup! diigo-cache: the public page only reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- diigo-cache/src/cache.ts | 28 +++++----- diigo-cache/src/index.ts | 115 +++++++++++++++++---------------------- 2 files changed, 62 insertions(+), 81 deletions(-) diff --git a/diigo-cache/src/cache.ts b/diigo-cache/src/cache.ts index 549e555..5b71f5c 100644 --- a/diigo-cache/src/cache.ts +++ b/diigo-cache/src/cache.ts @@ -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 { #step = 0; diff --git a/diigo-cache/src/index.ts b/diigo-cache/src/index.ts index 2bdc736..e61234a 100644 --- a/diigo-cache/src/index.ts +++ b/diigo-cache/src/index.ts @@ -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/`: 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 { 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 { const prefill = `?render=1&title=${encodeURIComponent("diigo cache: mint the session")}` + @@ -252,12 +232,13 @@ async function wirePage(webappId: string): Promise { }); } -/** 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 { const ct = request.headers.get("content-type") ?? ""; let webappId = ""; @@ -369,9 +350,11 @@ function fillLine(fill: FillStatus): string { return `

${esc(said)}

`; } -/** 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 { let cacheId = ""; try { @@ -465,10 +448,12 @@ async function dump(request: Request, env: Env): Promise { 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 { 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 {