OHAN-07: join and handles
how a person gets a handle, what a handle is, what it costs, how it lapses, moves to another mac and rotates its key. status: draft for v1, mon 26 oct 2026. the rules named (c8, c9, k5, k6, r8) are in OHAN-06 security; the card itself is OHAN-02. what comes later is named at the end.
a v1 draft. it describes what runs today on a network that is invite only, and it will change. every change is recorded, and a breaking one bumps the wire id (OHAN-00 §5).
1. the join, step by step
- the handle and the invite. the person says "join openthings as @maya with invite ohan-xxxx-xxxx-xxxx-xxxx". the client checks the handle rule (§2) before anything else. ohan.live is invite only for now (§9): with no code, the client says so and opens no window.
- the yes. a window on the person's screen says "join OHAN as @maya. this makes your keys on this mac and registers the handle on ohan.live. nothing about you is published." the confirm button reads "join as @maya"; cancel is the default; forty five seconds of silence is a no, so the window always closes before the host gives up on the call (about 60 seconds). before the window, the client checks that the handle is free and that the keychain answers, so the window is the last thing the call waits on and nobody says yes to a taken name. the host's own prompt (mcp elicitation) may ask instead of the window. a word in the chat or a tool argument is never the yes, and if neither the window nor the host's prompt can be shown, the join is refused with the reason (OHAN-06 §7).
- the keys. two keypairs are made on the device: ed25519 to sign, x25519 to receive sealed mail. the private parts are wrapped with a key kept in the macos keychain and written to the person's own files (k3). they never leave the device except in a backup (§5). the keys are written before the card is registered, so a lost answer leaves keys that can finish the join, never a handle with no keys.
- the card. the client builds the card (OHAN-02): the handle, the did, the two keys, the relays (ohan.live in v1),
listed: false, no profile fields, the stamp (§3), the holder's proof. - the registration.
POST /v0/profileon the directory, with the invite code in thex-ohan-inviteheader (never in the card, which is public). the box checks the body size, the protocol id, the handle rule, the invite (§9), the reserved list, the field limits, the proofs, the stamp, that the handle is free or already this key's, and the address cap (c8). it stores the card and serves it back unchanged, as json. - the result. the person hears: you are @maya; your page is
https://ohan.live/maya; anyone who knows the handle can write to you and nobody can browse to it; nothing about you was published; the export (§5) is offered; and the lapse line (§4): "open your agent at least once every six months, or accept one contact, or @maya may be deactivated".
a root (a person's own files) holds one handle. a second join on the same root is refused before the reserved rule is even looked at.
2. the handle rule
@then[a-z0-9][a-z0-9-]{0,30}: lowercase letters, digits and dashes, one to thirty one characters, starting with a letter or a digit. the client lowercases what the person typed and adds the@if they left it out.- an agent of a person adds one suffix:
@maya.opsis[a-z0-9-]{1,20}after a dot, withcontrollerset to the person's did on its card (OHAN-02 c4). v1 depth is one suffix. the openthings mcp does not make agent handles in v1. - the handle is the identity. a display name on a card is not verified and is absent in v1. the fingerprint of the signing key (eight groups) is what two people read to each other on a call the first time (OHAN-06 §12 line 4).
- reserved names (c8). the products:
openthings,ohan,opensoftware,opendm,openproof,openprofile,openhiring,nokk,nokklabs. the box's own words:admin,support,help,www,api,relay,spec,security,terms,claim,invite,v0,v1,healthz,assets,docs,install,llms,robots. and the names the operator holds for the people they belong to, which are a setting on the box, not in its code (the products and the box's words are in the code). a reserved name is registered only by the operator handing its first card to that person, with the headerx-ohan-reservedset to the operator's token. everyone else is told the name is reserved and may ask at the mailbox on the terms page. - a handle is never reassigned (§4, c9).
3. the stamp
a first registration carries stamp: { bits, nonce } on the card (the nip-13 shape): the sha256 of the card's jcs bytes without the proof has at least bits leading zero bits, and bits is at least the difficulty the box publishes in /.well-known/ohan.json (stamp.register, 20 in v1: about two seconds in plain node on a laptop). the committed bits counts, not a lucky hash. the client reads the difficulty before it grinds and says "a few seconds of work on purpose, so nobody can take a thousand names". a card update under the same key needs no new stamp. the box also caps registrations at 30 a day per address and 30 card writes a minute per address, counted on success, and card writes at 60 a day per handle (c13). both the address and the handle limits apply.
4. keeping a handle: refresh, re-register, lapse
- refresh (c9). on every start the client resolves its own handle. a card older than 150 days is refreshed: a new card under the same key, dated after the stored one, registered without a stamp. a person in use never lapses.
- re-register (r8). if the directory answers 404 for my own handle (it lost its data, or was rebuilt from an export), the client registers the card it kept, with the same key. the same key and a newer timestamp prove it is the same person. the person hears "ohan.live had lost your card, so i registered @maya again with the same key. your contacts notice nothing."
- lapse (c9). a card older than 180 days whose holder never posted an
allow(never accepted or added anyone) may be marked deactivated by the box. the did document then servesdeactivated: trueandGET /v0/resolve/@mayaanswers 410. a deactivated handle is never bound to another key. old signed material would otherwise name a stranger; did core treats non reassignment as a core property. the same key may return and reactivate the handle. own domain handles, which come later, will be the escape from a dead name.
5. backup and restore (k5)
on join the client offers a backup, and "back up my keys" makes one any time: both keypairs, the card and the contacts, in one file locked with a recovery key the client makes: 24 characters in six groups of four (crockford base32, so no i, l, o or u; 120 bits from the system's random source). the key is shown once, in one window, in a field the person can copy into a password manager; the window is also the yes, cancel is its default, and it closes by itself after fifty seconds. the key never reaches the chat, the consent log or a file. the file is scrypt (n 2^17, r 8, p 1) of the normalised key to a 32 byte key, then aes-256-gcm, with the handle on the outside so the restore window can name it, bound into the lock as associated data so it cannot be swapped. the consent log and the settings stay out of it. an importer refuses scrypt parameters above n 2^20, r 16, p 4.
the file goes to ~/openthings-backups/ and finder shows it, so the person can drag a copy to icloud drive or a usb stick: a backup on the mac it protects does not survive that mac. the client says, in plain words: whoever holds the file and the key is you; nobody, not openthings either, can reset the key.
restore on another mac asks for the key in one hidden window that names the handle; that window is the yes. case, spaces and dashes do not matter, and o, i and l are read as 0, 1 and 1. the client checks the file before it replaces anything: the identity is required, its card must verify against its own keys, its handle must match the one outside, and every contact must be well formed. a mac that already holds a different handle refuses (one handle per mac in v1). it restores the identity and the contacts and posts allow for the imported contacts to its relays (OHAN-04 r11), so they still reach the contact lane. the restored key fetches its own queue from the relays (messages already delivered to the first mac are not on the relay any more). two macs may hold the same key; each sees what it fetches first. "check my backup" opens the file with the key in the same window and changes nothing.
an export made before 26 oct 2026 with a passphrase the person chose still restores.
6. rotation (k6)
"rotate my key" (after a lost or stolen mac, or on a schedule the person likes) makes a new keypair and publishes a new card signed by the new key, dated after the previous one, carrying a second proof by the previous key: proofPurpose: assertionMethod, its verificationMethod the previous key as did:key:<multikey>#<multikey>, and previousCard naming the previous card's id. the box refuses a new key for a known handle without that proof. the old key is dead from that moment.
contacts follow a rotation when they next fetch the card or when the next message arrives: every envelope carries sender.card, the hash of the sender's current card, and a recipient holding a different hash re-resolves and verifies the rotation against the key it pinned before trusting the message. a new key without the proof is refused as an impostor (OHAN-05 g2b). a rotation is not instant; the limits page says so.
the lost mac flow is restore, then rotate, then the start refresh: the stolen key dies with the rotation. until a rotation key kept off the mac ships (k7), a thief who holds the backup and its recovery key can rotate first and lock the person out; the backup and its key therefore stay off the mac they protect.
7. the did:web mapping
@maya is did:web:ohan.live:maya; @maya.ops is did:web:ohan.live:maya:ops with controller: did:web:ohan.live:maya. a port in the domain is percent encoded. GET /maya/did.json serves a did document derived from the card: the verification methods as Multikey, authentication, assertionMethod, capabilityDelegation and capabilityInvocation on #sign, keyAgreement on #enc, assertionMethod only on #nostr when the person bound a nostr key (k2), and the services #card (type OhanCard, the resolve url) and #relays (type OhanRelay, the relay urls). the box's own identity is did:web:ohan.live at /.well-known/did.json with one service, #ohan of type OhanBox, pointing at /.well-known/ohan.json. in v1 the handle on its domain is the stable identifier.
8. what later versions add
own domain handles, a rotation key kept off the mac and listing come later.
9. invite only, for now
ohan.live is invite only while the network is small. a box says so in /.well-known/ohan.json (registration: { invite: "required", invitesPerMember }); a box run without --invite-only is open, as before.
- a new handle needs an invite code. a code looks like
ohan-xxxx-xxxx-xxxx-xxxx(80 random bits). it is sent in thex-ohan-inviteheader of the registration, works once, and expires. card updates, rotations and re-registration of a known key need no code. an agent under a member's root needs none. - the operator mints codes (
POST /v0/invites, with the operator's token): up to 50 at a time, valid up to 90 days (30 by default). a code may be bound to one handle; a bound code from the operator also unlocks a reserved name for the person it is held for, so the operator never hands out its token. - a member mints a few (
POST /v0/invite, a signed call from the member's key, as a fetch is signed): three per member, valid 14 days. the reference client asks in the window first ("make an invite with openthings"). - the box keeps only a hash of each code, who minted it, its expiry, and which handle used it. this is kept to stop abuse and is never published or served.
- wrong codes are limited to ten an hour per address. a right code never counts against the limit.
- what it is not: an invite is not a vouch and is not shown to anyone. who invited whom is not on any card. a code says nothing about the person who uses it.