// Add a device — mints a key-invite for this member and shows the sign-in // link + QR (the fragment carries the one-time secret; the server never sees // it). Extracted verbatim from shell.js. openInApp reuses the same mint to // hand this space straight to the installed app (issue #1089's handoff // cousin): an add-device link fired over the app scheme, claimed natively. import { verbs } from "/static/shellverbs.js"; import { session, brandingB } from "/static/shellstate.js"; import { buildInviteFragment } from "/static/invite.js"; import { deriveInvite, randomSecret, encrypt } from "/static/crypto.js"; import { qrCanvas } from "/static/qrcode.js"; import { launchEnrol } from "/static/backup.js"; import { appSchemeFrom } from "/static/ui.js"; // ---- add a device ------------------------------------------------------- // Mint a one-time sign-in link for THIS member: the new device gets this // user's keypair (wrapped under the fragment's secret — the server stores // only the wrapped blob and a claim hash). null = couldn't reach the server. async function mintDeviceLink() { try { const T = randomSecret(); const { id, wrapKey, claimHash } = await deriveInvite(T); // Ship this user's keypair (so the new device can read every thread). const wrapped = await encrypt(wrapKey, { pub: session.userPub, priv: session.userPriv }); const res = await fetch("/api/device-invites", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: session.token, id, wrapped, claim_hash: claimHash }), }); if (!res.ok) throw new Error("server"); // Neutral slug; the name rides in the #fragment (never sent to the server). return `${location.origin}/join/join#${buildInviteFragment(T, session.name, brandingB || "")}`; } catch { return null; } } export async function addDevice() { const link = await mintDeviceLink(); if (!link) return alert("Couldn't create a device link. Try again."); const backdrop = verbs.modal(); const card = backdrop.querySelector(".sheet"); const h = document.createElement("h2"); h.textContent = "Add a device"; const note = document.createElement("p"); note.className = "sub"; note.textContent = "Open this on another device to sign in there — or save it somewhere safe (like a password manager): it's a way back in if you lose this device. It works once, and anyone who uses it signs in as you."; const qr = document.createElement("div"); qr.className = "qr-wrap"; try { qr.appendChild(qrCanvas(link, { scale: 6, margin: 4 })); } catch { qr.textContent = "This link is too long to show as a QR code — use the link below."; } const box = document.createElement("div"); box.className = "key-box mono"; box.textContent = link; box.onclick = () => { const range = document.createRange(); range.selectNodeContents(box); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range); }; const copy = document.createElement("button"); copy.className = "secondary"; copy.textContent = "Copy link"; copy.onclick = async () => { try { await navigator.clipboard.writeText(link); copy.textContent = "Link copied ✓"; } catch { copy.textContent = "Select the link above to copy"; } setTimeout(() => (copy.textContent = "Copy link"), 2000); }; const done = document.createElement("button"); done.textContent = "Done"; done.onclick = () => backdrop.remove(); const row = document.createElement("div"); row.className = "row"; row.appendChild(copy); // Signing in with Eleven is the one-tap alternative to saving the link: // enrol (or update) this instance with the home server. Only shown when a // home server is configured — launchEnrol is a no-op otherwise. if (window.__homeOrigin) { const backup = document.createElement("button"); backup.className = "secondary"; backup.textContent = "Sign in with Eleven"; backup.onclick = () => { backdrop.remove(); // web navigates same-tab; native/PWA opens a new context launchEnrol(session); }; row.appendChild(backup); } row.appendChild(done); card.appendChild(h); card.appendChild(note); card.appendChild(qr); card.appendChild(box); card.appendChild(row); } // ---- open in the app ------------------------------------------------------ // The "Continue on web" regret path: hand this space to the installed app. // Same one-time link as addDevice, fired over the app scheme instead of shown // — the app pre-fills its Add-from-link confirm and this browser never shows // the secret. A page can't know whether anything caught the scheme, so the // sheet says what should have happened and keeps a copyable fallback. export async function openInApp() { const link = await mintDeviceLink(); if (!link) return alert("Couldn't create a sign-in link. Try again."); let scheme = "elevenmessenger"; try { scheme = appSchemeFrom(localStorage.getItem("lchat_app_scheme") || ""); } catch { /* no storage — the shipped app */ } // Assembled without a literal slash-pair: module-refs.test.mjs strips // comments before strings, so "//" inside a string desyncs its scan. location.href = scheme + ":" + "/".repeat(2) + "add?url=" + encodeURIComponent(link); const backdrop = verbs.modal(); const card = backdrop.querySelector(".sheet"); const h = document.createElement("h2"); h.textContent = "Handing over to the Eleven app"; const note = document.createElement("p"); note.className = "sub"; note.textContent = "The app opens with a one-time sign-in for this space — confirm it there and you're in as you. Nothing happened? You need the Eleven app (or its latest update); or copy the link below and paste it into the app's Add from link."; const copy = document.createElement("button"); copy.className = "secondary"; copy.textContent = "Copy the sign-in link"; copy.onclick = async () => { try { await navigator.clipboard.writeText(link); copy.textContent = "Link copied ✓"; } catch { copy.textContent = "Couldn't copy — use Add a device instead"; } setTimeout(() => (copy.textContent = "Copy the sign-in link"), 2000); }; const done = document.createElement("button"); done.textContent = "Done"; done.onclick = () => backdrop.remove(); const row = document.createElement("div"); row.className = "row"; row.appendChild(copy); row.appendChild(done); card.appendChild(h); card.appendChild(note); card.appendChild(row); }