diigo-cache: fill from a webkey page over Cap'n Web

Starting a fill no longer POSTs the cache id to /_fill. Instead:

- the created page links the cache's webkey, /cache/#<cache id>: a
  skeleton page, the same for every key, that reads the key from the
  fragment (never sent to the server or in a Referer)
- its script trades the key in-band for a capability over Cap'n Web at
  /api: CacheApi.cache(key) returns a CacheCap facet (fill, status,
  fingerprint, the public companion's fill) and never the DO's wiring
  methods; malformed and unknown keys get one refusal that never echoes
  the key, and each refusal is logged without it
- then calls startFill(): trade and call pipeline into one round trip
- the worker serves capnweb's browser build itself at /_capnweb.js (a
  wrangler Text rule), so no third-party script sits beside the key

The /_fill route and its forms are gone. Tests run the real capnweb
client against the worker (fetch routed to worker.fetch).

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 04:32:44 -05:00
1 parent fbe2538345
commit 8c221abe6f
9 files changed
+397 -112

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
/**
* @file the webkey page at /cache/ — a skeleton, identical for every key
* (docs/webkeys.md): the key rides the URL fragment, which browsers never
* send to the server or in a Referer. The inline module reads it, trades
* it for a capability (`api.cache(key)`, Cap'n Web over /api, in-band),
* and drives the fill job. Each round trip is its own HTTP batch session:
* calls made before an await travel together.
*/
export const CACHE_PAGE_HTML = `<!doctype html>
<html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">
<title>diigo cache</title></head>
<body>
<h1>diigo cache <code id="fingerprint"></code></h1>
<p id="problem" hidden></p>
<section id="private" hidden>
<h2>private cache</h2>
<p id="private-status">no fill job yet</p>
<button type="button" data-target="private" data-action="start">fill</button>
<button type="button" data-target="private" data-action="stop">stop</button>
</section>
<section id="public" hidden>
<h2>public cache</h2>
<p id="public-status">no fill job yet</p>
<button type="button" data-target="public" data-action="start">fill</button>
<button type="button" data-target="public" data-action="stop">stop</button>
</section>
<p><a href="/">← home</a></p>
<script type="module">
import { newHttpBatchRpcSession } from "/_capnweb.js";
const key = location.hash.slice(1);
const $ = (id) => document.getElementById(id);
const problem = (text) => {
$("problem").textContent = text;
$("problem").hidden = false;
};
/** a fresh batch session, traded for this key's capability */
const cap = () => newHttpBatchRpcSession("/api").cache(key);
/** the fill-job stub for a target */
const job = (target) => (target === "public" ? cap().publicCache() : cap());
const describe = (s) => {
if (!s) return "no fill job yet";
const of = Math.max(0, s.nextPage - 1) + " of " + (s.pages ?? "?") + " pages";
return {
running: "filling: " + of,
done: "filled: " + of,
stopped: "fill stopped: " + of,
failed: "fill failed at page " + s.nextPage + ": " + (s.error ?? "unknown error"),
}[s.state];
};
let polling = false;
async function refresh(hasPublic) {
const targets = hasPublic ? ["private", "public"] : ["private"];
const statuses = await Promise.all(targets.map((t) => job(t).fillStatus()));
targets.forEach((t, i) => ($(t + "-status").textContent = describe(statuses[i])));
const running = statuses.some((s) => s?.state === "running");
if (running && !polling) {
polling = true;
setTimeout(() => {
polling = false;
refresh(hasPublic).catch(report);
}, 3000);
}
}
function report(err) {
console.error({ event: "cache-page", error: String(err) });
problem(String(err));
}
async function main() {
if (!key) return problem("no key: this page's address should end in #<cache id>");
const c = cap();
const [fingerprint, hasPublic] = await Promise.all([c.fingerprint(), c.hasPublic()]);
$("fingerprint").textContent = fingerprint;
$("private").hidden = false;
$("public").hidden = !hasPublic;
for (const button of document.querySelectorAll("button[data-target]")) {
button.addEventListener("click", () => {
const { target, action } = button.dataset;
const j = job(target);
(action === "start" ? j.startFill() : j.stopFill())
.then(() => refresh(hasPublic))
.catch(report);
});
}
await refresh(hasPublic);
}
main().catch(report);
</script>
</body></html>`;