feat: pin a server from onboarding, derived from the address
This commit is contained in:
parent
3576e48b99
commit
1385ecb56c
3 changed files with 80 additions and 16 deletions
|
|
@ -39,12 +39,16 @@
|
||||||
<p>Identity is one 32-byte seed. Back it up: it is the only secret, and losing it means losing the address.</p>
|
<p>Identity is one 32-byte seed. Back it up: it is the only secret, and losing it means losing the address.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="create-identity">create a new identity</button>
|
<button id="create-identity">create a new identity</button>
|
||||||
<span>or restore from a saved seed:</span>
|
|
||||||
<input id="restore-seed" placeholder="64 hex characters" spellcheck="false">
|
|
||||||
<input id="restore-address" placeholder="alice@example.org (if already registered)"
|
|
||||||
spellcheck="false">
|
|
||||||
<button id="restore-identity">restore</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hint">or restore from a saved seed:</p>
|
||||||
|
<div class="row restore-row">
|
||||||
|
<label>seed<input id="restore-seed" placeholder="64 hex characters" spellcheck="false"></label>
|
||||||
|
<label>address (optional)<input id="restore-address" placeholder="alice@example.org, if already registered"
|
||||||
|
spellcheck="false"></label>
|
||||||
|
<label id="restore-key-label">server's public key
|
||||||
|
<input id="restore-key" placeholder="base32" spellcheck="false"></label>
|
||||||
|
<button id="restore-identity">restore</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<main id="app" hidden>
|
<main id="app" hidden>
|
||||||
|
|
|
||||||
|
|
@ -167,23 +167,48 @@ $("restore-identity").onclick = async () => {
|
||||||
return status(err.message, true);
|
return status(err.message, true);
|
||||||
}
|
}
|
||||||
showApp();
|
showApp();
|
||||||
// The address field is optional: restoring must succeed on its own even if
|
// The key/address fields are both optional, and neither can block the
|
||||||
// this fails (server not pinned yet, offline, typo) — recall can always be
|
// identity restore that already succeeded above — a typo, an offline server
|
||||||
// retried from settings afterward.
|
// or an unregistered address just leaves that one step to retry from
|
||||||
|
// settings afterward. No separate host field: it's already the part of the
|
||||||
|
// address after the @, so asking for it twice would just be one more place
|
||||||
|
// to enter it wrong (exactly how "pinned randogoth@smol.place" happened —
|
||||||
|
// a whole address typed into what was meant to be a bare host). Pinning has
|
||||||
|
// to happen before recall is attempted, since recall requires a pin.
|
||||||
|
const keyText = $("restore-key").value.trim();
|
||||||
const addressText = $("restore-address").value.trim();
|
const addressText = $("restore-address").value.trim();
|
||||||
let message = "identity restored.", warn = false;
|
const lines = ["identity restored."];
|
||||||
|
let warn = false;
|
||||||
|
|
||||||
|
if (keyText) {
|
||||||
|
if (!addressText) {
|
||||||
|
lines.push("enter the address above too — its server is derived from the part after the @.");
|
||||||
|
warn = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const host = parseAddress(addressText).host;
|
||||||
|
pinServerFromText(host, keyText);
|
||||||
|
lines.push(`pinned ${host}.`);
|
||||||
|
} catch (err) {
|
||||||
|
lines.push(err.message);
|
||||||
|
warn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (addressText) {
|
if (addressText) {
|
||||||
try {
|
try {
|
||||||
const addr = await recallAccount(addressText);
|
const addr = await recallAccount(addressText);
|
||||||
message += `\nrecalled ${addr.short}`;
|
lines.push(`recalled ${addr.short}.`);
|
||||||
renderAccount();
|
renderAccount();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message += `\ncouldn't recall that address: ${err.message}`;
|
lines.push(`couldn't recall that address: ${err.message}`);
|
||||||
warn = true;
|
warn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status(message, warn);
|
|
||||||
|
status(lines.join("\n"), warn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- header ---------------------------------------------------------------------
|
// --- header ---------------------------------------------------------------------
|
||||||
|
|
@ -852,14 +877,32 @@ function renderSettings() {
|
||||||
].filter(node => node != null));
|
].filter(node => node != null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared by the settings pin form and the optional host/key fields on setup —
|
||||||
|
// same validation, same unconditional pin. Both are direct, deliberate entry
|
||||||
|
// of a specific key (unlike importData's conflict-averse merge), so overwrite
|
||||||
|
// is the expected behavior here, not something to guard against.
|
||||||
|
function pinServerFromText(hostText, keyText) {
|
||||||
|
const host = hostText.trim().toLowerCase();
|
||||||
|
// A pin is keyed by bare host — connect() strips the port before looking one
|
||||||
|
// up (parseAddress splits user@host:port into three), so "user@host" or
|
||||||
|
// "host:port" here would silently pin a string nothing ever looks up again,
|
||||||
|
// exactly the mistake that produced "pinned <address>" followed by a
|
||||||
|
// confusing "no pinned key" from recall right after.
|
||||||
|
if (host.includes("@"))
|
||||||
|
throw new SmolError(`"${host}" looks like an address, not a server — the part after the @ goes here`);
|
||||||
|
if (host.includes(":"))
|
||||||
|
throw new SmolError(`"${host}": a pin is by host only, no port — just the part before the colon`);
|
||||||
|
const key = b32decode(keyText.trim());
|
||||||
|
if (key.length !== 32) throw new SmolError(`server key is ${key.length} bytes, expected 32`);
|
||||||
|
store.pinServer(host, key);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
$("pin-form").onsubmit = (event) => {
|
$("pin-form").onsubmit = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
try {
|
try {
|
||||||
const host = form.host.value.trim().toLowerCase();
|
const host = pinServerFromText(form.host.value, form.key.value);
|
||||||
const key = b32decode(form.key.value.trim());
|
|
||||||
if (key.length !== 32) throw new SmolError(`server key is ${key.length} bytes, expected 32`);
|
|
||||||
store.pinServer(host, key);
|
|
||||||
status(`pinned ${host}`);
|
status(`pinned ${host}`);
|
||||||
renderSettings();
|
renderSettings();
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
@ -1034,6 +1077,12 @@ function applyPresetServer() {
|
||||||
} catch { /* malformed preset: leave unpinned rather than block boot */ }
|
} catch { /* malformed preset: leave unpinned rather than block boot */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A preset deployment already knows its own server, so asking again on the
|
||||||
|
// setup screen would be redundant — and confusing in the other direction
|
||||||
|
// ("why do I need to type this if it's supposed to be automatic?"). A generic
|
||||||
|
// deployment has no preset, so this stays the only way to pin from onboarding.
|
||||||
|
if (PRESET_SERVER) $("restore-key-label").hidden = true;
|
||||||
|
|
||||||
applyPresetServer();
|
applyPresetServer();
|
||||||
applyTheme();
|
applyTheme();
|
||||||
store.identity() ? showApp() : showSetup();
|
store.identity() ? showApp() : showSetup();
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,17 @@ main { flex: 1; min-height: 0; display: flex; background: var(--bg); }
|
||||||
.setup h2 { margin: 0; font-weight: 400; }
|
.setup h2 { margin: 0; font-weight: 400; }
|
||||||
.setup p { color: var(--muted); max-width: 480px; }
|
.setup p { color: var(--muted); max-width: 480px; }
|
||||||
.setup .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
|
.setup .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
|
||||||
|
.setup .row.restore-row { align-items: flex-end; }
|
||||||
|
.setup label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 150px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.setup label input { font-family: var(--mono); font-size: 13px; }
|
||||||
|
|
||||||
/* --- dialogs --------------------------------------------------------------------- */
|
/* --- dialogs --------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue