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:
1 parent
2a1942fc2a
commit
164d3cb8cb
2 files changed
+62
-81
No files matched your search
+13
-15
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user