diff --git a/nix/bridge.nix b/nix/bridge.nix index b0dbfd3..6a32abf 100644 --- a/nix/bridge.nix +++ b/nix/bridge.nix @@ -21,7 +21,7 @@ assert lib.assertMsg ((serverHost == null) == (serverPublicKey == null)) "gsmol-bridge: serverHost and serverPublicKey must be set together, or not at all"; stdenvNoCC.mkDerivation { pname = "gsmol-bridge"; - version = "0.2.1"; + version = "0.2.0"; # Respects the repo's own .gitignore, so .venv/.devbox/.jj never enter the # store path even when building from an unclean local checkout. diff --git a/web/js/app.js b/web/js/app.js index 383bfdd..a227f46 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -946,19 +946,12 @@ async function sendMail(toText, subject, body, replyTo, anonymous) { // A signed Reply-To lets a first-time recipient name and answer us (§5.5 // allows unknown keys); "anonymous" omits it. if (account && !anonymous) fields.push(["Reply-To", account.uri(me.publicKey)]); - // §5.8: hand an accepted correspondent the token for our own mailbox, once, - // so a reply from them reaches our main tier. See store.tokenToDeliver for - // why this isn't on every message. - const owed = store.tokenToDeliver(addr.short); - if (owed) fields.push(["Accept", b32encode(tokenFor(master, owed))]); - // Trimmed before the payload is signed, so the signature covers exactly the - // text the author meant. A trailing newline is not content — but it becomes - // content the moment it is signed, and nothing downstream can strip it - // without invalidating the signature. smolboid, for one, refuses a post - // whose text carries a line break, because a published record is one line - // of a feed. + // §5.8: hand an accepted correspondent the token for our own mailbox, so a + // first reply from them reaches our main tier. + const accepted = store.accepted(addr.short); + if (accepted?.active) fields.push(["Accept", b32encode(tokenFor(master, accepted.identity))]); const bodyBytes = utf8Bytes(buildFrontmatter(fields.filter(([, v]) => v), - body.trim())); + body.replace(/\s+$/, "") + "\n")); const envelope = seal(me, recipient, bodyBytes); // §5.8: our token for their mailbox, if they have given us one. const held = store.tokenFrom(addr.short); @@ -969,9 +962,6 @@ async function sendMail(toText, subject, body, replyTo, anonymous) { } finally { opened.session.stream.close(); } - // Only now, with the message taken by their server: a send that threw - // earlier leaves the token still owing, so the next attempt carries it. - if (owed) store.markDelivered(addr.short); // §5.6: the ephemeral is gone, so keep a copy sealed to ourselves. await store.storeMessage("sent", { id: hex(messageId(envelope)), diff --git a/web/js/store.js b/web/js/store.js index 70931c0..e2924b8 100644 --- a/web/js/store.js +++ b/web/js/store.js @@ -247,32 +247,7 @@ export function allContacts() { export function accepted(address) { const a = (load().accepted || {})[address]; - return a ? { identity: b32decode(a.identity), active: a.active, delivered: !!a.delivered } : null; -} - -// The identity to derive a token for, but only while that token still needs -// handing over. Null once they have it. -// -// §5.8 has the owner *deliver* each token to its correspondent; it does not -// say every message carries one. Attaching it to all of them puts a secret in -// the body of ordinary mail, and a recipient that republishes what it gets -// can't carry one: a published record is checked by rebuilding the exact -// bytes that were signed, and the token must not be among them. -export function tokenToDeliver(address) { - const a = (load().accepted || {})[address]; - return a && a.active && !a.delivered ? b32decode(a.identity) : null; -} - -// Records that this correspondent now holds our token, so later messages -// carry no secret. Deliberately not undone by block(): they keep the value -// either way, and re-accepting hands back the same one, because it is derived -// from the master and the identity frozen at acceptance. -export function markDelivered(address) { - update(state => { - const table = { ...(state.accepted || {}) }; - if (table[address]) table[address] = { ...table[address], delivered: true }; - state.accepted = table; - }); + return a ? { identity: b32decode(a.identity), active: a.active } : null; } // Admit a contact to the main tier. The identity is frozen at acceptance — a @@ -286,10 +261,6 @@ export function accept(address, identityBytes) { identity: previous?.identity ?? b32encode(identityBytes), active: true, addedAt: previous?.addedAt ?? Date.now(), - // A contact accepted before this field existed is treated as still - // owing, so the next message carries the token once more. That is what - // every message did before, so it cannot break anything that works. - delivered: !!previous?.delivered, }; state.accepted = table; });