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>
|
||||
<div class="row">
|
||||
<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>
|
||||
<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 id="app" hidden>
|
||||
|
|
|
|||
|
|
@ -167,23 +167,48 @@ $("restore-identity").onclick = async () => {
|
|||
return status(err.message, true);
|
||||
}
|
||||
showApp();
|
||||
// The address field is optional: restoring must succeed on its own even if
|
||||
// this fails (server not pinned yet, offline, typo) — recall can always be
|
||||
// retried from settings afterward.
|
||||
// The key/address fields are both optional, and neither can block the
|
||||
// identity restore that already succeeded above — a typo, an offline server
|
||||
// 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();
|
||||
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) {
|
||||
try {
|
||||
const addr = await recallAccount(addressText);
|
||||
message += `\nrecalled ${addr.short}`;
|
||||
lines.push(`recalled ${addr.short}.`);
|
||||
renderAccount();
|
||||
renderSettings();
|
||||
} catch (err) {
|
||||
message += `\ncouldn't recall that address: ${err.message}`;
|
||||
lines.push(`couldn't recall that address: ${err.message}`);
|
||||
warn = true;
|
||||
}
|
||||
}
|
||||
status(message, warn);
|
||||
|
||||
status(lines.join("\n"), warn);
|
||||
};
|
||||
|
||||
// --- header ---------------------------------------------------------------------
|
||||
|
|
@ -852,14 +877,32 @@ function renderSettings() {
|
|||
].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) => {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
try {
|
||||
const host = form.host.value.trim().toLowerCase();
|
||||
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);
|
||||
const host = pinServerFromText(form.host.value, form.key.value);
|
||||
status(`pinned ${host}`);
|
||||
renderSettings();
|
||||
form.reset();
|
||||
|
|
@ -1034,6 +1077,12 @@ function applyPresetServer() {
|
|||
} 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();
|
||||
applyTheme();
|
||||
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 p { color: var(--muted); max-width: 480px; }
|
||||
.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 --------------------------------------------------------------------- */
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue