eleven / messenger Public

home: reframe the home page as "Sign in with Eleven" (#308)

The home server stores an encrypted backup of a member's per-instance keys,
but from the member's side that passkey *is* their Eleven identity: one
face/fingerprint check unlocks the list of spaces they belong to. Reword the
manage page (/) to match — "Sign in with Eleven" instead of "Create a backup",
"Your Eleven spaces" instead of a server list, and make each space a link so
you can jump straight back into any of them. The E2EE rule is unchanged: the
server still only holds opaque ciphertext, "sign in" is just the friendly name
for unlocking the sealed key vault.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 57b8c479b9a4a48e10a512968c51439df184f853 parent 6cd43c8
2 files changed, +35 −29
  • home/web/pages/index.html +23 −19
  • home/web/static/home.js +12 −10
diff --git a/home/web/pages/index.html b/home/web/pages/index.html
index 9b3925a..c45192c 100644
--- a/home/web/pages/index.html
+++ b/home/web/pages/index.html
@@ -5,16 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#f6f7fb" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#15171c" media="(prefers-color-scheme: dark)">
-<title>Eleven backup</title>
+<title>Sign in with Eleven</title>
<link rel="stylesheet" href="/static/home.css">
<style>
- /* page-specific: the server + device lists and the add form */
+ /* page-specific: the spaces + device lists and the add form */
ul.servers { list-style: none; margin: .75rem 0 0; padding: 0; }
ul.servers li {
display: flex; align-items: baseline; gap: .5rem; padding: .5rem 0;
border-top: 1px solid var(--border);
}
- ul.servers li .name { font-weight: 600; }
+ ul.servers li a.name { font-weight: 600; color: inherit; }
ul.servers li .url { color: var(--muted); font-size: .85rem; overflow-wrap: anywhere; }
ul.devices { list-style: none; margin: .75rem 0 0; padding: 0; }
ul.devices li {
@@ -27,28 +27,24 @@
ul.devices li button { padding: 6px 12px; font-size: 13px; }
.addform { display: flex; gap: .5rem; margin-top: .75rem; flex-wrap: wrap; }
.addform input { flex: 1; min-width: 8rem; }
+ #vault h3 { font-size: 14px; margin: 16px 0 0; font-family: var(--font-display); }
</style>
</head>
<body class="manage">
<main>
- <h1>Eleven backup</h1>
- <p class="sub">Your keys to your chats, sealed behind your passkeys. The
- backup is encrypted on your device — not even this server can see inside.</p>
-
- <section id="create-section">
- <h2>Create a backup passkey</h2>
- <p>Saves a passkey on this device and starts an empty, encrypted backup.
- Use it later to get back into your chats on a new device.</p>
- <button id="create">Create a backup passkey</button>
- <p class="status" id="create-status"></p>
- </section>
+ <h1>Sign in with Eleven</h1>
+ <p class="sub">One passkey — your face or fingerprint — opens all your Eleven
+ spaces. Your keys are sealed on your device before they're stored; not even
+ this server can see inside.</p>
<section id="restore-section">
- <h2>Restore</h2>
- <p>Unlock your backup with a passkey and see the chats saved in it.</p>
- <button id="restore">Unlock with passkey</button>
+ <h2>Sign in</h2>
+ <p>Unlock with your passkey to see your Eleven spaces and jump back into
+ any of them.</p>
+ <button id="restore">Sign in with passkey</button>
<p class="status" id="restore-status"></p>
<div id="vault" class="hidden">
+ <h3>Your Eleven spaces</h3>
<ul class="servers" id="server-list"></ul>
<form class="addform" id="add-form">
<input id="add-name" placeholder="Name" autocomplete="off">
@@ -59,10 +55,18 @@
</div>
</section>
+ <section id="create-section">
+ <h2>New here?</h2>
+ <p>Create your Eleven passkey on this device. Spaces you save from their
+ chat pages appear here, ready to sign back into anywhere.</p>
+ <button id="create">Create your Eleven passkey</button>
+ <p class="status" id="create-status"></p>
+ </section>
+
<section id="devices-section">
<h2>Devices</h2>
- <p>Devices that can read and update this backup. Revoking one requires a
- fresh passkey check.</p>
+ <p>Devices signed in with your passkey. Revoking one requires a fresh
+ passkey check.</p>
<button id="devices" class="secondary">Show devices</button>
<ul class="devices hidden" id="device-list"></ul>
<p class="status" id="devices-status"></p>
diff --git a/home/web/static/home.js b/home/web/static/home.js
index 7d7a136..d587a4e 100644
--- a/home/web/static/home.js
+++ b/home/web/static/home.js
@@ -1,4 +1,5 @@
-// The home page: create a backup passkey, restore, manage devices. All crypto
+// The home page: sign in with a passkey, list your spaces, manage devices.
+// (Under the hood a "sign-in" unlocks the encrypted key backup.) All crypto
// is client-side (see crypto.js); the WebAuthn ceremonies + blob I/O live in
// backup-core.js (shared with the enrolment popup). Status and errors always
// render inline in each section — never a floating toast.
@@ -85,7 +86,7 @@ async function createBackup() {
version: "0",
};
await saveVault();
- setStatus("create-status", "Backup created and protected by your new passkey.", "ok");
+ setStatus("create-status", "Your Eleven passkey is ready. Spaces you save will appear here.", "ok");
renderVault();
}
@@ -98,25 +99,25 @@ async function restore() {
state.credId = credId;
const got = await fetchBlob(token);
if (!got) {
- setStatus("restore-status", "Signed in, but there's no backup stored yet.", "error");
+ setStatus("restore-status", "Signed in, but no spaces are saved to this passkey yet.", "error");
return;
}
if (!prf) {
- setStatus("restore-status", "Signed in, but this browser didn't return a PRF result, so the backup can't be decrypted here.", "error");
+ setStatus("restore-status", "Signed in, but this browser didn't return a PRF result, so your spaces can't be unlocked here.", "error");
return;
}
const kek = await kekFromPRF(prf);
const { keys } = unpackBlob(got.bytes);
const wrapped = keys[credId];
if (!wrapped) {
- setStatus("restore-status", "This passkey isn't enrolled in the backup's keys.", "error");
+ setStatus("restore-status", "This passkey isn't one of this account's keys.", "error");
return;
}
dekKey = await unwrapDEK(wrapped, kek);
state.dek = await exportDEK(dekKey);
const opened = await openBlob(got.bytes, dekKey);
vault = { obj: opened.obj, keys: opened.keys, version: got.version };
- setStatus("restore-status", "Backup unlocked.", "ok");
+ setStatus("restore-status", "Signed in.", "ok");
renderVault();
}
@@ -127,13 +128,14 @@ function renderVault() {
const servers = vault.obj.servers || [];
if (!servers.length) {
const li = document.createElement("li");
- li.textContent = "No servers in this backup yet.";
+ li.textContent = "No spaces saved yet — save one from its chat page and it'll appear here.";
list.append(li);
}
for (const s of servers) {
const li = document.createElement("li");
- const name = document.createElement("span");
+ const name = document.createElement("a");
name.className = "name";
+ name.href = s.url;
name.textContent = s.name || new URL(s.url).host;
const url = document.createElement("span");
url.className = "url";
@@ -147,7 +149,7 @@ function renderVault() {
async function showDevices() {
if (!state.token) {
- setStatus("devices-status", "Restore (or create) first — the device list needs a signed-in session.", "error");
+ setStatus("devices-status", "Sign in first — the device list needs a signed-in session.", "error");
return;
}
const { devices } = await apiJSON("/api/devices", {
@@ -235,7 +237,7 @@ if (state.token && state.dek) {
dekKey = await importDEK(state.dek);
const opened = await openBlob(got.bytes, dekKey);
vault = { obj: opened.obj, keys: opened.keys, version: got.version };
- setStatus("restore-status", "Backup unlocked (session).", "ok");
+ setStatus("restore-status", "Signed in (this session).", "ok");
renderVault();
} catch { /* token or DEK went stale; the buttons still work */ }
})();