diigo-cache: webkey page logs in with the password manager

/cache/ without a fragment is now a login form: petname
(autocomplete=username) and cache id (type=password,
autocomplete=current-password), the shape password managers fill from
the entries the save button makes. Its inputs have no name, so a native
submit (script failed) sends nothing; the key stays in memory, out of
the address bar and history. Home links to it.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 04:48:15 -05:00
1 parent 961515d8db
commit efaf74023d
3 files changed
+37 -2

No files matched your search

+25 -2
View File
@@ -5,6 +5,11 @@
* it for a capability (`api.cache(key)`, Cap'n Web over /api, in-band), * 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: * and drives the fill job. Each round trip is its own HTTP batch session:
* calls made before an await travel together. * calls made before an await travel together.
*
* Without a fragment, the page is a login form a password manager fills
* (petname as username, cache id as password). Its inputs have no
* `name`, so a native submit (script failed) sends nothing; the key
* stays in memory, out of the address bar and history.
*/ */
export const CACHE_PAGE_HTML = `<!doctype html> export const CACHE_PAGE_HTML = `<!doctype html>
@@ -13,6 +18,11 @@ export const CACHE_PAGE_HTML = `<!doctype html>
<body> <body>
<h1>diigo cache <code id="fingerprint"></code></h1> <h1>diigo cache <code id="fingerprint"></code></h1>
<p id="problem" hidden></p> <p id="problem" hidden></p>
<form id="login" hidden>
<p><label>petname <input id="login-petname" autocomplete="username" size="24"></label></p>
<p><label>cache id <input id="login-key" type="password" autocomplete="current-password" size="24" required></label></p>
<p><button type="submit">open</button></p>
</form>
<section id="private" hidden> <section id="private" hidden>
<h2>private cache</h2> <h2>private cache</h2>
<p id="private-status">no fill job yet</p> <p id="private-status">no fill job yet</p>
@@ -29,7 +39,7 @@ export const CACHE_PAGE_HTML = `<!doctype html>
<script type="module"> <script type="module">
import { newHttpBatchRpcSession } from "/_capnweb.js"; import { newHttpBatchRpcSession } from "/_capnweb.js";
const key = location.hash.slice(1); let key = location.hash.slice(1);
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const problem = (text) => { const problem = (text) => {
$("problem").textContent = text; $("problem").textContent = text;
@@ -71,8 +81,21 @@ function report(err) {
problem(String(err)); problem(String(err));
} }
/** no fragment: ask for the key (the password manager fills it in) */
function login() {
const form = $("login");
form.hidden = false;
form.addEventListener("submit", (event) => {
event.preventDefault();
key = $("login-key").value.trim();
if (!key) return;
form.hidden = true;
main().catch(report);
});
}
async function main() { async function main() {
if (!key) return problem("no key: this page's address should end in #<cache id>"); if (!key) return login();
const c = cap(); const c = cap();
const [fingerprint, hasPublic] = await Promise.all([c.fingerprint(), c.hasPublic()]); const [fingerprint, hasPublic] = await Promise.all([c.fingerprint(), c.hasPublic()]);
$("fingerprint").textContent = fingerprint; $("fingerprint").textContent = fingerprint;
+1
View File
@@ -582,6 +582,7 @@ nobody here opens it. Mint lands you on the wire step, id pre-filled.</li>
<p><button type="submit">wire</button></p> <p><button type="submit">wire</button></p>
</form> </form>
<p>the ids this mints are for <b>you</b>: copy them somewhere you control (password manager)</p> <p>the ids this mints are for <b>you</b>: copy them somewhere you control (password manager)</p>
<p><a href="/cache/">open a cache</a> — fill it, watch progress (your password manager can fill in the cache id)</p>
${dumpForm} ${dumpForm}
${result} ${result}
</body></html>`, </body></html>`,
+11
View File
@@ -240,6 +240,17 @@ describe("fill via webkey and Cap'n Web (goal 1e)", () => {
expect(html).toContain('<meta name="referrer" content="no-referrer">'); expect(html).toContain('<meta name="referrer" content="no-referrer">');
}); });
it("without a fragment, the webkey page is a login form a password manager can fill, which posts nothing", async () => {
const html = await (await call("/cache/")).text();
const form = html.match(/<form id="login"[^]*?<\/form>/)?.[0] ?? "";
expect(form).toMatch(/<input[^>]* autocomplete="username"/);
expect(form).toMatch(/<input[^>]* type="password"[^>]* autocomplete="current-password"/);
// no name attributes: a native submit carries no key anywhere
expect(form).not.toMatch(/\bname=/);
expect(form).not.toMatch(/\baction=/);
expect(html).toContain("event.preventDefault()");
});
it("serves capnweb's browser build itself (no third-party script beside the key)", async () => { it("serves capnweb's browser build itself (no third-party script beside the key)", async () => {
const res = await call("/_capnweb.js"); const res = await call("/_capnweb.js");
expect(res.headers.get("content-type")).toMatch(/javascript/); expect(res.headers.get("content-type")).toMatch(/javascript/);