| index 3347837..26a92b1 100644 |
| --- a/home/web/static/home.js |
| +++ b/home/web/static/home.js |
| @@ -1,17 +1,20 @@ |
| // The home landing page: one "Sign in with Eleven" button that unlocks your |
| -// list of spaces. A brand-new device (no passkey yet) can create one from the |
| -// fallback link. All crypto is client-side (crypto.js); the WebAuthn ceremonies |
| -// + blob I/O live in backup-core.js. Status renders inline — never a toast. |
| +// list of spaces, plus a Sign out that forgets this browser's session. All |
| +// crypto is client-side (crypto.js); the WebAuthn ceremonies + blob I/O live in |
| +// backup-core.js. Status renders inline — never a toast. |
| // |
| -// "Sign in" is the friendly name for unlocking the encrypted key vault: the |
| -// server only ever holds opaque ciphertext, never a usable key. |
| +// Home is a sign-in surface only: a new member gets their passkey through the |
| +// join / save-a-space flow (/enroll), not here — so there's no "create an |
| +// account" path to clutter the page. "Sign in" is the friendly name for |
| +// unlocking the encrypted key vault; the server only ever holds opaque |
| +// ciphertext, never a usable key. |
| |
| import { |
| - kekFromPRF, generateDEK, exportDEK, importDEK, wrapDEK, unwrapDEK, |
| + kekFromPRF, exportDEK, importDEK, unwrapDEK, |
| sealBlob, unpackBlob, openBlob, |
| } from "/static/crypto.js"; |
| import { |
| - apiJSON, beginLogin, beginRegister, runRegister, runLogin, fetchBlob, putBlob, |
| + apiJSON, beginLogin, runLogin, fetchBlob, putBlob, |
| } from "/static/backup-core.js"; |
| |
| // ---- session state ---------------------------------------------------------- |
| @@ -58,14 +61,9 @@ let spacesEnabled = false; |
| // navigator.credentials.* synchronously — Safari drops the user activation |
| // across an intervening network await and rejects with NotAllowedError. |
| let loginBegin = null; |
| -let registerBegin = null; |
| function prefetchLogin() { |
| beginLogin().then((v) => { loginBegin = v; }).catch(() => { loginBegin = null; }); |
| } |
| -function prefetchRegister() { |
| - if (registerBegin) return; |
| - beginRegister().then((v) => { registerBegin = v; }).catch(() => { registerBegin = null; }); |
| -} |
| |
| // ---- blob save ---------------------------------------------------------------- |
| |
| @@ -125,43 +123,32 @@ async function signIn() { |
| showVault(); |
| } |
| |
| -// ---- create (fallback for a brand-new device) -------------------------------- |
| - |
| -async function createPasskey() { |
| - $("create").disabled = true; |
| - setStatus("Waiting for your passkey…", "busy"); |
| - let reg; |
| - try { |
| - reg = await runRegister(registerBegin); |
| - } catch (e) { |
| - registerBegin = null; prefetchRegister(); // spent — refresh for a retry |
| - if (e.message === "no-prf") { |
| - setStatus("This passkey manager can't protect your keys here (no PRF support). You can still use add-device links to move between devices.", "error"); |
| - $("create").disabled = false; |
| - return; |
| - } |
| - throw e; |
| - } |
| - state.token = reg.token; |
| - state.credId = reg.credId; |
| +// ---- sign out ----------------------------------------------------------------- |
| |
| - const kek = await kekFromPRF(reg.prf); |
| - dekKey = await generateDEK(); |
| - state.dek = await exportDEK(dekKey); |
| - vault = { |
| - obj: { v: 1, servers: [] }, |
| - keys: { [reg.credId]: await wrapDEK(dekKey, kek) }, |
| - version: "0", |
| - }; |
| - await saveVault(); |
| - setStatus("Your Eleven passkey is ready. Spaces you save will appear here.", "ok"); |
| - showVault(); |
| +// signOut forgets this browser's session — the link token and DEK held only in |
| +// sessionStorage — and returns to the signed-out landing. It doesn't revoke the |
| +// passkey or touch the backup: signing back in is one tap. (Revoking a device |
| +// is a separate, ceremony-gated action.) |
| +function signOut() { |
| + sessionStorage.removeItem("home_token"); |
| + sessionStorage.removeItem("home_dek"); |
| + sessionStorage.removeItem("home_cred"); |
| + vault = null; |
| + dekKey = null; |
| + $("vault").classList.add("hidden"); |
| + $("space-section").classList.add("hidden"); |
| + $("new-space-result").classList.add("hidden"); |
| + $("server-list").textContent = ""; |
| + $("signed-out").classList.remove("hidden"); |
| + $("signin").disabled = false; |
| + setStatus("Signed out.", ""); |
| + prefetchLogin(); // ready a fresh ceremony for the next sign-in |
| } |
| |
| // ---- spaces list -------------------------------------------------------------- |
| |
| function showVault() { |
| - $("newhere").classList.add("hidden"); |
| + $("signed-out").classList.add("hidden"); |
| $("vault").classList.remove("hidden"); |
| if (spacesEnabled) $("space-section").classList.remove("hidden"); |
| const list = $("server-list"); |
| @@ -278,34 +265,26 @@ async function addServer(e) { |
| // ---- wiring ------------------------------------------------------------------- |
| |
| const onSignInError = (err) => { |
| - // Safari surfaces both a cancelled prompt and a missing credential as |
| - // NotAllowedError, so we can't tell "you cancelled" from "nothing to sign in |
| - // with". Offer the create fallback on either and keep the message honest. |
| + // A cancelled prompt and a missing credential both surface as NotAllowedError, |
| + // so keep the message neutral. New members get their passkey by joining or |
| + // saving a space from its chat, not here — no create button to offer. |
| if (err && err.name === "NotAllowedError") { |
| - setStatus("No passkey was used. If this device is new, create your Eleven passkey below.", ""); |
| + setStatus("Sign-in was cancelled, or there's no Eleven passkey on this device yet.", ""); |
| } else { |
| setStatus((err && err.message) || String(err), "error"); |
| } |
| - $("newhere").classList.remove("hidden"); |
| - prefetchRegister(); |
| $("signin").disabled = false; |
| prefetchLogin(); // options were spent — refresh for a retry |
| }; |
| |
| -const onCreateError = (err) => { |
| - if (err && err.name === "NotAllowedError") setStatus("Passkey prompt was dismissed.", ""); |
| - else setStatus((err && err.message) || String(err), "error"); |
| - $("create").disabled = false; |
| -}; |
| - |
| if (!window.PublicKeyCredential) { |
| setStatus("This browser doesn't support passkeys.", "error"); |
| $("signin").disabled = true; |
| } else { |
| prefetchLogin(); |
| $("signin").addEventListener("click", () => signIn().catch(onSignInError)); |
| - $("create").addEventListener("click", () => createPasskey().catch(onCreateError)); |
| } |
| +$("signout").addEventListener("click", signOut); |
| $("new-space").addEventListener("click", () => createSpace().catch((err) => { |
| if (err && err.name === "NotAllowedError") spaceStatus("Passkey prompt was dismissed.", ""); |
| else spaceStatus((err && err.message) || String(err), "error"); |