Compare commits
3 commits
3a03fab876
...
d19568c1c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d19568c1c0 | ||
|
|
6c8419aa32 | ||
|
|
fcb54e2b40 |
3 changed files with 46 additions and 7 deletions
|
|
@ -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.0";
|
||||
version = "0.2.1";
|
||||
|
||||
# Respects the repo's own .gitignore, so .venv/.devbox/.jj never enter the
|
||||
# store path even when building from an unclean local checkout.
|
||||
|
|
|
|||
|
|
@ -946,12 +946,19 @@ 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, 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))]);
|
||||
// §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.
|
||||
const bodyBytes = utf8Bytes(buildFrontmatter(fields.filter(([, v]) => v),
|
||||
body.replace(/\s+$/, "") + "\n"));
|
||||
body.trim()));
|
||||
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);
|
||||
|
|
@ -962,6 +969,9 @@ 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)),
|
||||
|
|
|
|||
|
|
@ -247,7 +247,32 @@ export function allContacts() {
|
|||
|
||||
export function accepted(address) {
|
||||
const a = (load().accepted || {})[address];
|
||||
return a ? { identity: b32decode(a.identity), active: a.active } : null;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Admit a contact to the main tier. The identity is frozen at acceptance — a
|
||||
|
|
@ -261,6 +286,10 @@ 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;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue