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.
Asking someone
Pick a space you both belong to, and the person to ask:
const { space: pairSpace, carried } = await node.contacts.ask(space.id, did, { note: 'Hi from the co-op!' });This makes a new private space for two, puts them on your list with it, and
leaves a request for them in the shared space. space, here pairSpace, is
the space for two.
carried is false when none of your always-on carriers held the space in
time. Then they can join only while one of your devices is online.
Answering a request
On their side, requests lists what was left for them, and accept joins:
const [request] = await node.contacts.requests(space.id);
if (request) {
const contact = await node.contacts.accept(space.id, request.key);
}There is no reply. Joining the space for two is the answer, and you each have the other on your list.
The list
const contacts = await node.contacts.list();get, put, remove, block and the rest are in
NodeContacts, and
each contact is a
ContactView. Its
name is what you call them. They never see it.
remove and block leave the space for two, giving up your place there,
unless another contact names the same space. block keeps them on the list
with blocked: true, and hides their requests in every space and in your
inbox. others lists accounts in the space for two besides the two of you,
from its members, profiles and connected peers.
A contact's username is what their domain answered when you found them by
it. Nothing checks it again: once a name lapses and someone else claims it,
a lookup leads to them, and your contact keeps the old one.
Inviting a contact into a space
await node.contacts.invite(did, space.id, { role: 'member' });This leaves an invite for them in your space for two. They must have joined that space first. A request nobody has answered yet is refused, so you never open a space to whoever a username led to before you have seen who they are. A blocked contact is refused too.
On their side, invites() lists what contacts left for them, newest first,
and join takes them in:
const [invite] = await node.contacts.invites();
if (invite) await node.contacts.join(invite.pairSpace, invite.key);Each item is a
SpaceInviteView.
Only invites to spaces you don't hold yet are listed. Only the contact's own
records count, and each invite is checked as joining checks it: signed by
the contact, for the space whose constitution it carries. That is where
space and name come from. Invites from someone blocked are not shown.
invites() also closes and deletes your own invites that the contact has
joined with.
Who may ask
ask and accept need a session note that grants /account/write for the
whole account. An app granted contacts can see the list and the requests,
but can't ask or accept. See
spec 06 §2.5.
How ask works
- It creates a private
teamspace named"<my name> & <their name>", and opens aneditorinvite to it. - It waits up to 3 seconds for an online carrier to hold the space
(
spaces.carried). - It writes a
std.contactfor them, with that space. - It leaves the request, a sealed knock, as a
std.contact-request.
It returns { space, request, carried }: the space for two, the request's
record key, and whether a carrier held it. It needs the other person's
contactKey on their profile in that space. accept joins the invite, and
writes a std.contact for the asker with the space for two.
requests(space) skips blocked senders, and knocks whose space for two you
already hold.
std.contact
The list lives in the account's contacts space, derived with the label
minca/contacts, one record per person:
std.contact (rules: onePer ["did"])
{ did: string ≤256, name: string ≤200, space?: string ≤256, note?: string ≤2000,
blocked?: boolean }
record key = records.onePerKey(contactsSpace, "std.contact", { did })
= "one:" + hex(HMAC(oneKey, "std.contact\ndid=" + canonical(did))[0..20))
// a shared key, spec 02 §7.3: onePer without @authorspace is the id of the space for two. A reader ignores a record whose key
is not the one its did derives. It also ignores one whose root is not the
account, as for every record in the account's derived spaces (spec 03
§13.1).
The contact key
A request is sealed to the other person's contact key: the public half,
published as contactKey on their profile in the space. A node holds the
private half only when it was given it (config.contactKey). An agent never
is. See spec 01 §10.1 for how it is derived, and spec 03 §11 for the
profile.
std.contact-request
std.contact-request (no rules; who may write one is the space's grants)
{ to: <the askee's contact key, as on their profile here, ≤64>, sealed: string ≤16000 }sealed is a knock sealed to to, whose invite is to the new space for two.
ask defines the collection first if the space lacks it, which needs
/collection/define. In a private space the body is sealed with the space
key too, so other members see that someone asked, not whom.
The receiver opens a request only when to is its own contact key, the
knock checks out, and the knock's account is the record's root and not its
own. A knock someone else copied into another record is not theirs to leave,
and opens as nothing.
The list and the request are conventions: peers that have never heard of them sync and judge them like any other record. The knock inside is protocol, the same however it is handed over. See spec 07 §6.
std.space-invite
An invite to a space, left in a space for two: { space, invite }, with
onePer: ['space'], so inviting again to the same space replaces it.
It stays open until its maker's node sees the contact in the space and
closes it, the next time it reads invites().
Until then, a contact removed from the space can rejoin with it, as with any open role invite (#170). Anyone else with a place in the space for two can read it (#313). A view-only invite carries the space key and can't be closed (#172).
Planned: requests that can be taken back
Today remove and block leave your request standing, so they can still
accept it, into a space nobody holds. And spaces.leave on a space for two
leaves the std.contact that names it. The plan:
std.contactgainsasked?: { space: string ≤256, key: string ≤256 }, whereaskleft the request.removeandblockdelete that request, when the space is still held and the record still stands, before leaving the space for two.spaces.leaveon a space astd.contactnames does whatremovedoes.ContactViewgainswaiting: boolean, true while the other account has no member record in the space for two.
Tracked in #40.
Spaces
A space is a shared place: the records in it, and the people who may read and write them. On this page we'll make a space, invite someone to it, and then look at how a space decides who may do what.
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.