The inbox
An inbox is an address you hand out on purpose, so that someone you share no space with can ask to become your contact. They knock, you accept, and you both end up in a private space for two.
Opening your inbox
const inbox = await node.inbox.open();inbox.code is what you hand out: in a link (#inbox=…), a QR code or a
bio. Or claim a username, so people can find you by a name
they can say aloud.
Knocking
On the other side, knock with the code, a link with it, or a username:
const { space, carried } = await node.inbox.knock(code, { note: 'We met at the meetup' });space is the new space for two. carried is false when none of your
always-on carriers held it in time, so they can join only while one of your
devices is online.
node.inbox.sent() lists your knocks nobody has joined yet. Once they join,
they become a contact on your side by themselves.
Answering knocks
const [knock] = await node.inbox.knocks();
if (knock) {
const contact = await node.inbox.accept(knock.id);
}accept joins the space for two and puts them on your list. There is no
reply: joining is the answer. dismiss(id) lets a knock go, without
blocking whoever knocked.
knocks() fetches what is waiting. It doesn't watch, though the mailbox
could. The default mailbox client opens a fresh WebSocket to a relay for each
call. Every call is listed in node.md.
Changing your inbox
Call open again to change where knocks wait (1 to 3 mailboxes) or the name
the code gives:
await node.inbox.open({ name: 'Leif', places: ['wss://relay.minca.social'] });The keys stay the same, so a code you already handed out keeps working.
rotate() replaces the inbox. It clears what waits in the old one, and the
old code leads nowhere. Nothing ties the new inbox to the old.
clear() purges every waiting knock and keeps the inbox open.
Who may
Knocking and accepting create or join a space, so they need a note that
grants /account/write for the whole account, as contacts.ask does, and
the contact key.
The inbox is the person's. A node whose note lacks /account/write, such as
an agent's, refuses to open, replace, clear, read, knock, accept or dismiss.
A home never gives an agent the contact key in the first place. See
spec 06 §2.8.
The exchange
Here is what happens when Leif knocks on Anna's inbox:
Anna mailbox(es) Leif
│ inbox.open() → std.inbox, code │
│ ── code, out of band (a link, a QR code, a bio) ─────────────────────────────▶ │
│ spaces.create(private), invite(editor) │
│ spaces.carried: a carrier holds it │
│ ◀── drop(topic, sealed knock) ── to each │
│ std.knock { space, invite } │
│ fetch(topic) ──▶ ◀── mail [knock] │
│ open + check (spec 07 §6) │
│ accept: spaces.join(invite); std.contact { did: Leif, space } │
│ purge(knock) ──▶ │
│ ═════════════════ the space for two syncs, through Leif's carrier if he's away ▶ │
│ Anna is a member: std.contact { did: Anna }, invite │
│ closed, std.knock deleted │Knocking
The knocker:
- Reads the code, checks its signature, and refuses its own inbox.
- Creates a private space named
"<own name> & <code name>"with theteampreset, and makes an editor invite. - If one of its carriers is online, waits up to 3 seconds for it to hold the
space and invite (
spaces.carried). With no carrier, or none in time, it goes on and returnscarried: false. - Seals the knock and drops it in every mailbox the code names. Any one of them may lose it: the reference relay keeps its mailbox in memory, so a restart loses it.
- If no mailbox answers
dropped, leaves the space and fails. - Otherwise writes
std.knock { space, name, invite }in its contacts space.
Opening
Besides the checks the protocol requires, the owner's devices skip knocks from the account itself, from anyone blocked on its contact list, and whose space for two it already holds, accepted here or on another device.
Accepting
The owner joins the invite and writes
std.contact { did: <the knocker's account>, name, space }. It keeps a name
it already had for that account. Then it purges the knock from the
mailboxes.
Settling
For each std.knock, the knocker's devices look at the members of its space
for two. The first other account with a place there is whoever answered,
since only the holder of the inbox key could open the invite. They write
std.contact { did: <that account>, name: <their profile name, else the code's>, space },
close the invite, and delete the std.knock, so nobody joins after. Anyone
else who joined too shows up in contacts.others.
The invite works for anyone until then, so it must reach nobody else. If the owner passes it on, or loses a device before accepting, whoever joins first is taken for them. The owner could just as well forward the knock itself.
Expiry
A std.knock nobody joined within the knock TTL (14 days) plus a day is let
go: the knocker leaves the space and deletes the record. One whose space is
no longer held is deleted.
The keys, the inbox code, the topic, the mailbox and the sealed knock are protocol. See spec 07.
std.inbox
The inbox's settings are a std.inbox record in the account's contacts
space, which only the account can find and every one of its devices holds.
The knocker keeps a std.knock in its own.
| Collection | Fields | Rules | Where |
|---|---|---|---|
std.inbox | gen ≥ 0 and places (1–3) required; name ≤64 | — | Owner's contacts space |
std.knock | space ≤256, name ≤64, invite ≤12000 (all required) | onePer: ['space'] | Knocker's contacts space |
gensays which inbox: both keys are derived from it.placesare mailbox URLs, each passing the inbox code's relay check.nameis the name the code gives whoever knocks.
A device takes as the inbox only a verified std.inbox whose root is the
account itself, and of those the one with the highest gen, then the newest.
Changing the places or the name writes a new version of that record. rotate
writes gen + 1.
These records are conventions: peers that have never heard of them sync and judge them like any other record. Only one account's devices need to agree on them. See spec 07 §1.
Not yet built
- Live knocks: watching the inbox's topic, so a knock shows as it arrives.
- Retrying a knock that reached no mailbox, or whose mailboxes lost it, until someone joins or it expires. A retry is a fresh knock, signed and dropped at once.
- Mailboxes that last: a host that keeps its accounts' inbox topics across restarts, as it keeps short links.
Contacts
A contact is someone you share a private space for two with. Use this page to ask someone you already share a space with to become a contact. For someone you share no space with, use their inbox.
Warrants
A warrant (std.warrant) is proof that an account's key signed something every peer refuses and no honest app would sign. Your app uses warrants to show the members of a space that someone broke…