eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Add a device — mints a key-invite for this member and shows the sign-in
2// link + QR (the fragment carries the one-time secret; the server never sees
3// it). Extracted verbatim from shell.js. openInApp reuses the same mint to
4// hand this space straight to the installed app (issue #1089's handoff
5// cousin): an add-device link fired over the app scheme, claimed natively.
6import { verbs } from "/static/shellverbs.js";
7import { session, brandingB } from "/static/shellstate.js";
8import { buildInviteFragment } from "/static/invite.js";
9import { deriveInvite, randomSecret, encrypt } from "/static/crypto.js";
10import { qrCanvas } from "/static/qrcode.js";
11import { launchEnrol } from "/static/backup.js";
12import { appSchemeFrom } from "/static/ui.js";
13
14// ---- add a device -------------------------------------------------------
15
16// Mint a one-time sign-in link for THIS member: the new device gets this
17// user's keypair (wrapped under the fragment's secret — the server stores
18// only the wrapped blob and a claim hash). null = couldn't reach the server.
19async function mintDeviceLink() {
20 try {
21 const T = randomSecret();
22 const { id, wrapKey, claimHash } = await deriveInvite(T);
23 // Ship this user's keypair (so the new device can read every thread).
24 const wrapped = await encrypt(wrapKey, { pub: session.userPub, priv: session.userPriv });
25 const res = await fetch("/api/device-invites", {
26 method: "POST",
27 headers: { "Content-Type": "application/json" },
28 body: JSON.stringify({ token: session.token, id, wrapped, claim_hash: claimHash }),
29 });
30 if (!res.ok) throw new Error("server");
31 // Neutral slug; the name rides in the #fragment (never sent to the server).
32 return `${location.origin}/join/join#${buildInviteFragment(T, session.name, brandingB || "")}`;
33 } catch {
34 return null;
35 }
36}
37
38export async function addDevice() {
39 const link = await mintDeviceLink();
40 if (!link) return alert("Couldn't create a device link. Try again.");
41 const backdrop = verbs.modal();
42 const card = backdrop.querySelector(".sheet");
43 const h = document.createElement("h2");
44 h.textContent = "Add a device";
45 const note = document.createElement("p");
46 note.className = "sub";
47 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.";
48 const qr = document.createElement("div");
49 qr.className = "qr-wrap";
50 try {
51 qr.appendChild(qrCanvas(link, { scale: 6, margin: 4 }));
52 } catch {
53 qr.textContent = "This link is too long to show as a QR code — use the link below.";
54 }
55 const box = document.createElement("div");
56 box.className = "key-box mono";
57 box.textContent = link;
58 box.onclick = () => {
59 const range = document.createRange();
60 range.selectNodeContents(box);
61 const sel = getSelection();
62 sel.removeAllRanges();
63 sel.addRange(range);
64 };
65 const copy = document.createElement("button");
66 copy.className = "secondary";
67 copy.textContent = "Copy link";
68 copy.onclick = async () => {
69 try {
70 await navigator.clipboard.writeText(link);
71 copy.textContent = "Link copied ✓";
72 } catch {
73 copy.textContent = "Select the link above to copy";
74 }
75 setTimeout(() => (copy.textContent = "Copy link"), 2000);
76 };
77 const done = document.createElement("button");
78 done.textContent = "Done";
79 done.onclick = () => backdrop.remove();
80 const row = document.createElement("div");
81 row.className = "row";
82 row.appendChild(copy);
83 // Signing in with Eleven is the one-tap alternative to saving the link:
84 // enrol (or update) this instance with the home server. Only shown when a
85 // home server is configured — launchEnrol is a no-op otherwise.
86 if (window.__homeOrigin) {
87 const backup = document.createElement("button");
88 backup.className = "secondary";
89 backup.textContent = "Sign in with Eleven";
90 backup.onclick = () => {
91 backdrop.remove(); // web navigates same-tab; native/PWA opens a new context
92 launchEnrol(session);
93 };
94 row.appendChild(backup);
95 }
96 row.appendChild(done);
97 card.appendChild(h);
98 card.appendChild(note);
99 card.appendChild(qr);
100 card.appendChild(box);
101 card.appendChild(row);
102}
103
104// ---- open in the app ------------------------------------------------------
105
106// The "Continue on web" regret path: hand this space to the installed app.
107// Same one-time link as addDevice, fired over the app scheme instead of shown
108// — the app pre-fills its Add-from-link confirm and this browser never shows
109// the secret. A page can't know whether anything caught the scheme, so the
110// sheet says what should have happened and keeps a copyable fallback.
111export async function openInApp() {
112 const link = await mintDeviceLink();
113 if (!link) return alert("Couldn't create a sign-in link. Try again.");
114 let scheme = "elevenmessenger";
115 try {
116 scheme = appSchemeFrom(localStorage.getItem("lchat_app_scheme") || "");
117 } catch { /* no storage — the shipped app */ }
118 // Assembled without a literal slash-pair: module-refs.test.mjs strips
119 // comments before strings, so "//" inside a string desyncs its scan.
120 location.href = scheme + ":" + "/".repeat(2) + "add?url=" + encodeURIComponent(link);
121
122 const backdrop = verbs.modal();
123 const card = backdrop.querySelector(".sheet");
124 const h = document.createElement("h2");
125 h.textContent = "Handing over to the Eleven app";
126 const note = document.createElement("p");
127 note.className = "sub";
128 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.";
129 const copy = document.createElement("button");
130 copy.className = "secondary";
131 copy.textContent = "Copy the sign-in link";
132 copy.onclick = async () => {
133 try {
134 await navigator.clipboard.writeText(link);
135 copy.textContent = "Link copied ✓";
136 } catch {
137 copy.textContent = "Couldn't copy — use Add a device instead";
138 }
139 setTimeout(() => (copy.textContent = "Copy the sign-in link"), 2000);
140 };
141 const done = document.createElement("button");
142 done.textContent = "Done";
143 done.onclick = () => backdrop.remove();
144 const row = document.createElement("div");
145 row.className = "row";
146 row.appendChild(copy);
147 row.appendChild(done);
148 card.appendChild(h);
149 card.appendChild(note);
150 card.appendChild(row);
151}
152