capsite/diigo-cache: the wire page hands the owner the store id
webapp's mint now answers a store id (x-webapp-store-id) beside the instance id: authority to rotate or revoke the diigo cookie. The wire page shows it apart from the instance id, with its own copy button, petname, and password-manager save; the cache never gets it. A webapp that sends none gets a note on the page and a warning in the log. Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
3f711a5f4b
commit
4dd3fe6bde
1 file changed
+36
-17
+36
-17
@@ -174,11 +174,13 @@ async function proxyMint(request: Request, env: Env): Promise<Response> {
|
|||||||
const detail = (await res.text()).slice(0, 200);
|
const detail = (await res.text()).slice(0, 200);
|
||||||
return home(env, { error: `mint said ${res.status}: ${detail}` }, null);
|
return home(env, { error: `mint said ${res.status}: ${detail}` }, null);
|
||||||
}
|
}
|
||||||
// webapp answers {id} (+ x-webapp-id); the id rides the header, the
|
// webapp answers {id, storeId} (+ x-webapp-id, x-webapp-store-id);
|
||||||
// wire page is ours
|
// the ids ride the headers, the wire page is ours
|
||||||
const id = res.headers.get("x-webapp-id");
|
const id = res.headers.get("x-webapp-id");
|
||||||
if (!id) return home(env, { error: "mint produced no id" }, null);
|
if (!id) return home(env, { error: "mint produced no id" }, null);
|
||||||
return await wirePage(id);
|
const storeId = res.headers.get("x-webapp-store-id");
|
||||||
|
if (!storeId) console.warn({ event: "mint", outcome: "no store id: this session cannot be rotated" });
|
||||||
|
return await wirePage(id, storeId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error({ event: "mint", error: String(err) });
|
console.error({ event: "mint", error: String(err) });
|
||||||
return home(env, { error: `mint failed: ${String(err)}` }, null);
|
return home(env, { error: `mint failed: ${String(err)}` }, null);
|
||||||
@@ -188,22 +190,37 @@ async function proxyMint(request: Request, env: Env): Promise<Response> {
|
|||||||
/** the credential-save helper, once per page (see ./pm-save.ts) */
|
/** the credential-save helper, once per page (see ./pm-save.ts) */
|
||||||
const PM_SAVE_JS = pmSave.toString();
|
const PM_SAVE_JS = pmSave.toString();
|
||||||
|
|
||||||
/** the suggested petname gets the holder's local hh:mm (the worker only
|
/** the suggested petname (in the input with id `petnameId`) gets the
|
||||||
* knows UTC), so two saves the same day are told apart at a glance */
|
* holder's local hh:mm (the worker only knows UTC), so two saves the same
|
||||||
const PETNAME_TIME_JS = `{
|
* day are told apart at a glance */
|
||||||
const p = document.getElementById("petname");
|
function petnameTimeJs(petnameId: string): string {
|
||||||
|
return `{
|
||||||
|
const p = document.getElementById(${JSON.stringify(petnameId)});
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
p.value += " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
p.value += " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
/** the onclick that hands pmSave its browser powers */
|
|
||||||
function pmSaveClick(value: string): string {
|
|
||||||
return `pmSave(this, ${JSON.stringify(value)}, { petname: document.getElementById("petname"), credentials: navigator.credentials, PasswordCredential: window.PasswordCredential, log: console })`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the wire step, with the freshly-minted webapp id pre-filled */
|
/** the onclick that hands pmSave its browser powers, saving under the
|
||||||
async function wirePage(webappId: string): Promise<Response> {
|
* petname in the input with id `petnameId` */
|
||||||
|
function pmSaveClick(value: string, petnameId: string): string {
|
||||||
|
return `pmSave(this, ${JSON.stringify(value)}, { petname: document.getElementById(${JSON.stringify(petnameId)}), credentials: navigator.credentials, PasswordCredential: window.PasswordCredential, log: console })`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the wire step, with the freshly-minted webapp id pre-filled, and the
|
||||||
|
* store id (rotating the cookie) for the account owner to keep */
|
||||||
|
async function wirePage(webappId: string, storeId: string | null): Promise<Response> {
|
||||||
const fingerprint = await nick(webappId);
|
const fingerprint = await nick(webappId);
|
||||||
|
const storeSection = storeId
|
||||||
|
? `<h2>keep: the cookie's store id</h2>
|
||||||
|
<p>This one is yours alone, not the cache's: with it you can rotate the
|
||||||
|
diigo session (log in again, paste the new cookie) or revoke it. Fingerprint
|
||||||
|
<code>${esc(await nick(storeId))}</code>
|
||||||
|
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(storeId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
|
||||||
|
<p><label>your petname for it <input id="store-petname" autocomplete="off" size="30" value="${esc(`diigo cookie store ${await nick(storeId)}`)}"></label></p>
|
||||||
|
<script>${petnameTimeJs("store-petname")}</script>
|
||||||
|
<p><button type="button" onclick='${pmSaveClick(storeId, "store-petname")}'>save to password manager</button></p>`
|
||||||
|
: `<p><b>note:</b> webapp sent no store id, so this session's cookie cannot be rotated; when it expires, mint again.</p>`;
|
||||||
const body = `<!doctype html>
|
const body = `<!doctype html>
|
||||||
<html><head><meta charset="utf-8"><title>diigo cache: step 2 — wire the cache</title></head>
|
<html><head><meta charset="utf-8"><title>diigo cache: step 2 — wire the cache</title></head>
|
||||||
<body>
|
<body>
|
||||||
@@ -212,8 +229,10 @@ async function wirePage(webappId: string): Promise<Response> {
|
|||||||
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(webappId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
|
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(webappId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
|
||||||
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo webapp ${fingerprint}`)}"></label>
|
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo webapp ${fingerprint}`)}"></label>
|
||||||
<small>(private to you: the password-manager username; never sent to the server)</small></p>
|
<small>(private to you: the password-manager username; never sent to the server)</small></p>
|
||||||
<script>${PETNAME_TIME_JS}</script>
|
<script>${petnameTimeJs("petname")}</script>
|
||||||
<p><button type="button" onclick='${pmSaveClick(webappId)}'>save to password manager</button></p>
|
<p><button type="button" onclick='${pmSaveClick(webappId, "petname")}'>save to password manager</button></p>
|
||||||
|
${storeSection}
|
||||||
|
<h2>wire</h2>
|
||||||
<p>choose the cache mode, then <b>wire</b> — the next page shows the
|
<p>choose the cache mode, then <b>wire</b> — the next page shows the
|
||||||
<b>cache id</b> (copy that too), after which you land back on home:</p>
|
<b>cache id</b> (copy that too), after which you land back on home:</p>
|
||||||
<form method="post" action="/_wire">
|
<form method="post" action="/_wire">
|
||||||
@@ -320,8 +339,8 @@ async function wiredPage(cacheId: string, note: string, hasPublic: boolean): Pro
|
|||||||
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(cacheId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
|
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(cacheId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
|
||||||
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo cache ${fingerprint}`)}"></label>
|
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo cache ${fingerprint}`)}"></label>
|
||||||
<small>(private to you: the password-manager username; never sent to the server)</small></p>
|
<small>(private to you: the password-manager username; never sent to the server)</small></p>
|
||||||
<script>${PETNAME_TIME_JS}</script>
|
<script>${petnameTimeJs("petname")}</script>
|
||||||
<p><button type="button" onclick='${pmSaveClick(cacheId)}'>save to password manager</button></p>
|
<p><button type="button" onclick='${pmSaveClick(cacheId, "petname")}'>save to password manager</button></p>
|
||||||
<p>${note}</p>
|
<p>${note}</p>
|
||||||
<p><a href="/cache/#${esc(cacheId)}">open this cache</a> — its <b>webkey</b>
|
<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>
|
(bookmark it, or save the id above): fill it, watch progress${hasPublic ? ", fill its public companion" : ""}.</p>
|
||||||
|
|||||||
Reference in new issue
Block a user