feat: recall an already-registered address without REGISTER

This commit is contained in:
randogoth 2026-09-26 20:31:22 +03:00
parent a244dab94e
commit a24c143fd0
2 changed files with 42 additions and 1 deletions

View file

@ -94,7 +94,7 @@
<section>
<h3><span id="step-server" class="step" data-n="1">1</span> your server</h3>
<p class="hint">Pin the key of the server that will hold your mail. Get it from its operator over a
channel you already trust — registering and fetching both refuse an unpinned server (SPEC.md §4).</p>
channel you already trust — registering, recalling and fetching all refuse an unpinned server (SPEC.md §4).</p>
<form id="pin-form">
<label>host<input name="host" placeholder="example.org" spellcheck="false" required></label>
<label>public key<input name="key" placeholder="base32" spellcheck="false" required></label>
@ -111,6 +111,16 @@
<label>invite token<input name="token" placeholder="if required"></label>
<button class="filled">register</button>
</form>
<details>
<summary>already registered elsewhere?</summary>
<p class="hint">Restoring a seed brings back the identity, not the memory of what address it was
registered under in a different browser — the server was never asked. Recall it here: this
looks the address up and checks it resolves to this identity's key, without registering anything.</p>
<form id="recall-form">
<label>address<input name="address" placeholder="alice@example.org" spellcheck="false" required></label>
<button>recall</button>
</form>
</details>
</section>
<section>

View file

@ -883,6 +883,37 @@ $("register-form").onsubmit = async (event) => {
}
};
// Restoring a seed brings back the identity, not the memory of what address a
// *different* browser registered it under — "account" is local-only state,
// never asked of the server. This binds it without REGISTER: RESOLVE the
// address and require it name this exact key, so a typo or someone else's
// address can't silently misfile outgoing Reply-To / fetch under it.
$("recall-form").onsubmit = async (event) => {
event.preventDefault();
const form = event.target;
try {
const me = store.identity();
if (!me) throw new SmolError("no identity yet");
const addr = parseAddress(form.address.value.trim());
const opened = await connect(addr, { requirePin: true });
let current;
try {
({ identity: current } = await resolveOp(opened.session, addr.user));
} finally {
opened.session.stream.close();
}
if (!timingSafeEqual(current, me.publicKey))
throw new SmolError(`${addr.short} resolves to a different key — not this identity`);
store.setAccount(addr);
status(`recalled ${addr.short}\nshare: ${addr.uri(me.publicKey)}`);
renderAccount();
renderSettings();
form.reset();
} catch (err) {
status(err.message, true);
}
};
async function rotateIdentity() {
const me = store.identity();
const addr = accountAddress();