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:
1 parent
fbe2538345
commit
8c221abe6f
9 files changed
+397
-112
No files matched your search
+24
-71
@@ -13,6 +13,10 @@
|
||||
*/
|
||||
|
||||
import { WorkerEntrypoint } from "cloudflare:workers";
|
||||
import { newWorkersRpcResponse } from "capnweb";
|
||||
import { CacheApi } from "./api.js";
|
||||
import { CAPNWEB_BROWSER_JS } from "./capnweb-browser.js";
|
||||
import { CACHE_PAGE_HTML } from "./cache-page.js";
|
||||
import { DiigoCache, PublicCache } from "./cache.js";
|
||||
import type { FillStatus } from "./cache.js";
|
||||
import type { Item } from "./cache.js";
|
||||
@@ -96,9 +100,17 @@ export default {
|
||||
return publishCache(request, env);
|
||||
}
|
||||
|
||||
// the fill job (goal 1e): owner-only, cache id in the body
|
||||
if (url.pathname === "/_fill" && request.method === "POST") {
|
||||
return fillRoute(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 });
|
||||
@@ -108,9 +120,9 @@ export default {
|
||||
}
|
||||
|
||||
// GET / is inert (dumps happen on POST submits). The cache id is a
|
||||
// webkey: it never rides the URL (query strings leak into history/
|
||||
// Referer/logs — see docs/webkeys.md); the wired form POST is how
|
||||
// it travels.
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
@@ -293,18 +305,6 @@ async function wire(request: Request, env: Env): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
/** the fill job's buttons: one per cache the holder can fill (the id
|
||||
* rides a hidden field, never the URL) */
|
||||
function fillForms(cacheId: string, hasPublic: boolean): string {
|
||||
const form = (target: string, label: string) => `<form method="post" action="/_fill">
|
||||
<input type="hidden" name="cacheId" value="${esc(cacheId)}">
|
||||
<input type="hidden" name="target" value="${target}">
|
||||
<button type="submit">${label}</button></form>`;
|
||||
return `<p>fetch every page now, in the background (a few minutes for thousands of bookmarks):</p>
|
||||
${form("private", "fill the private cache")}
|
||||
${hasPublic ? form("public", "fill the public cache") : ""}`;
|
||||
}
|
||||
|
||||
/** the created-cache page: ids + save/copy buttons + the pointer home */
|
||||
async function wiredPage(cacheId: string, note: string, hasPublic: boolean): Promise<Response> {
|
||||
const fingerprint = await nick(cacheId);
|
||||
@@ -319,11 +319,13 @@ async function wiredPage(cacheId: string, note: string, hasPublic: boolean): Pro
|
||||
<script>${PETNAME_TIME_JS}</script>
|
||||
<p><button type="button" onclick='${pmSaveClick(cacheId)}'>save to password manager</button></p>
|
||||
<p>${note}</p>
|
||||
${fillForms(cacheId, hasPublic)}
|
||||
<p><a href="/cache/#${esc(cacheId)}">open this cache</a> — its <b>webkey</b>
|
||||
(bookmark it, or save the id above): fill it, watch progress${hasPublic ? ", fill its public companion" : ""}.</p>
|
||||
<p>the cache id is the only way back to this cache — saved (above) it lives
|
||||
with the rest of your credentials. it is a <b>webkey</b>: it never travels
|
||||
in URLs (query strings leak to history/Referer/logs); the dump POST carries
|
||||
it in its body instead. the webapp id rides inside the cache's wiring;
|
||||
with the rest of your credentials. it is a <b>webkey</b>: it never rides a
|
||||
query string or path (those leak to history/Referer/logs). the link above
|
||||
carries it in the fragment, which browsers never send; the dump POST
|
||||
carries it in its body. the webapp id rides inside the cache's wiring;
|
||||
hold it separately only if you want another cache off the same session.</p>
|
||||
<a href="/">← home (paste the cache id into the dump form)</a>
|
||||
<script>${PM_SAVE_JS}</script>
|
||||
@@ -343,55 +345,6 @@ function fillLine(fill: FillStatus): string {
|
||||
return `<p>${esc(said)}</p>`;
|
||||
}
|
||||
|
||||
/** POST /_fill: start or stop the fill job of the holder's private cache
|
||||
* (target=private, the default) or of its public companion
|
||||
* (target=public). JSON in → JSON out; a form post gets a small page. */
|
||||
async function fillRoute(request: Request, env: Env): Promise<Response> {
|
||||
const ct = request.headers.get("content-type") ?? "";
|
||||
const wantsJson = ct.includes("application/json");
|
||||
let fields: Record<string, string>;
|
||||
try {
|
||||
fields = wantsJson
|
||||
? Object.fromEntries(Object.entries((await request.json()) as Record<string, unknown>).map(([k, v]) => [k, String(v ?? "")]))
|
||||
: Object.fromEntries([...(await request.formData()).entries()].map(([k, v]) => [k, String(v)]));
|
||||
} catch (err) {
|
||||
return bad(`bad input: ${String(err)}`);
|
||||
}
|
||||
const { cacheId = "", target = "private", action = "start" } = fields;
|
||||
if (!isWebkey(cacheId)) return bad("no cache id supplied (POST body: cacheId)");
|
||||
if (target !== "private" && target !== "public") return bad("target: expected private or public");
|
||||
if (action !== "start" && action !== "stop") return bad("action: expected start or stop");
|
||||
|
||||
const cache = env.CACHE_DO.get(env.CACHE_DO.idFromString(idFromWebkey(cacheId)));
|
||||
const wiring = await cache.wiring();
|
||||
if (!wiring) return Response.json({ error: "unknown cache id" }, { status: 404 });
|
||||
let stub: { startFill(): Promise<FillStatus>; stopFill(): Promise<FillStatus> } = cache;
|
||||
if (target === "public") {
|
||||
if (!wiring.publicId) return bad("this cache has no public companion; publish it first");
|
||||
stub = env.PUBLIC_DO.get(env.PUBLIC_DO.idFromString(idFromWebkey(wiring.publicId)));
|
||||
}
|
||||
let status: FillStatus;
|
||||
try {
|
||||
status = action === "start" ? await stub.startFill() : await stub.stopFill();
|
||||
} catch (err) {
|
||||
console.error({ event: "fill", target, action, error: String(err) });
|
||||
return bad(`could not ${action} the fill: ${String(err)}`);
|
||||
}
|
||||
const code = action === "start" ? 202 : 200;
|
||||
if (wantsJson) return Response.json(status, { status: code });
|
||||
return new Response(
|
||||
`<!doctype html>
|
||||
<html><head><meta charset="utf-8"><title>diigo cache: fill</title></head>
|
||||
<body>
|
||||
<h1>fill (${esc(target)} cache)</h1>
|
||||
${fillLine(status)}
|
||||
<p>it runs in the background; the progress shows on the public page, or post this form again to check.</p>
|
||||
<a href="/">← home</a>
|
||||
</body></html>`,
|
||||
{ status: code, headers: { "content-type": "text/html;charset=utf-8" } },
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
|
||||
Reference in new issue
Block a user