OHAN-04: the relay

how sealed envelopes wait for someone who is offline, and how a recipient reads them. wire ids ohan.event.1 (the sealed event) and ohan.relay.1 (the api). status: draft for v1, mon 26 oct 2026; supersedes the demo's relay.v0, which a v1 box does not speak. rules e and r are in OHAN-06 security.

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 sealed event

{
  "protocol": "ohan.event.1",
  "seal": "x25519-sha256-aesgcm-1",   // the seal format; a later seal gets its own name
  "to": "z6Mk…",                       // the recipient's SIGNING key. queues are addressed and authorised by it
  "epk": "<ephemeral x25519 public, hex>",
  "ts": "…", "exp": "…",               // exp at most 30 days plus five minutes ahead; ts within the same window
  "kind": "sealed",
  "from_key": "z6Mk…",                 // the sender's signing key
  "stamp": { "bits": 18, "nonce": "…" }, // required when the sender is not in the recipient's allow list (r1): at least the published stranger bits. the reference client stamps every event
  "payload": "<base64: 12 byte nonce + 16 byte tag + ciphertext>",
  "id": "<sha256 of the jcs of everything above>",
  "signature": "<ed25519 by from_key over the jcs of everything above, base58btc>"
}

the seal (e2, e3, e5). the sender makes an ephemeral x25519 key and derives the aes key sha256(shared || epk || to_enc): the raw 32 byte x25519 shared secret with the recipient's #enc key, the raw 32 byte ephemeral public key, and the raw 32 byte x25519 key of the recipient's #enc. the plaintext is a 4 byte big endian length, the envelope's json, then zeros to the next 256 byte boundary; aes-256-gcm with a random 96 bit nonce and associated data to || epk || seal, where to is the recipient's signing key as its multikey string in ascii (z6Mk…), epk the raw 32 byte ephemeral key, and seal the seal string in ascii. so the key derivation uses the #enc key and the associated data uses the signing key. a shared secret of all zeros (a small order key) is refused on both sides. the recipient's #enc key never appears on the event; the relay knows only the signing key. the box annotates a stored event with seq, its position in that queue; seq is outside the signed bytes and never part of the id.

2. the api

callwhobodythe box checksanswers
POST /v0/publishanyonean evente4; on a box with a directory, to and from_key each belong to a registered card; the lane (r1): if from_key is in the recipient's allow list, the contact lane (2000 per recipient, 500 per sender); else the stranger lane (100 per recipient, 3 per sender, a stamp of at least the published bits; the lowest stamp is evicted when full); the byte caps (8 mb per sender key, 32 mb per recipient); the address rate{ ok, seq }; { ok, duplicate, seq }; 400 with the reason; 429 or 503 with a reason that never names a lane
GET /v0/inbox?since=<seq>the holder of toheader authorization: ohan-fetch <base64 of {relay, to, since, ts, nonce, signature}>the signature by to over the jcs of the rest; relay equals this box's origin; ts within five minutes; (to, nonce) unseen for five minutes{ events, seq, more }, 200 a page
POST /v0/deliveredthe holder of to{ to, ids, ts, nonce, signature }as the fetchthe box drops those events; { ok, dropped }
POST /v0/invitea member (the holder of to, with a card here)a signed call { to, ts, nonce, signature }one invite code of the member's few (OHAN-07 §9)
POST /v0/invitesthe operator, with its token{ count, handle?, days? }invite codes; one may be bound to a handle
POST /v0/allowthe holder of to{ to, allow: [keys], disallow: [keys], ts, nonce, signature }as the fetch; on a box with a directory, to belongs to a registered cardthe recipient's allow list on this box is updated; { ok }. the box cannot tell a removal from a block
GET /.well-known/ohan.jsonanyonethe ids it speaks, stamp: { register: 20, stranger: 18 }, limits, counts

nothing is dropped on fetch alone (r3). a client keeps one cursor per relay and deduplicates by event id and envelope id. no url ever carries a key or a signature (r4).

who may publish. a box with a directory accepts a publish only to a recipient key that belongs to a registered card, and only from a sender key that belongs to a registered card. a relay only box (§4) has no directory, so it skips that check.

capacity (r1b). besides the lanes, the box caps undelivered bytes per sender key (8 mb across all recipients) and per recipient key (32 mb). when a recipient's 32 mb is full, strangers' events go first, the weakest stamp first; a contact's event may then push out the oldest event of the other sender holding the most bytes for that recipient. when the box as a whole is full, it evicts the oldest events of the sender key holding the most undelivered bytes, instead of refusing everyone; if that sender is the one publishing, it refuses with 503.

what the answers say. the publish answer does not say which lane an event went into, and an error answer does not name a lane. the reference client stamps every event at the published stranger difficulty, so it is never refused for a missing stamp. one signal remains: a sender who hits a per sender limit can infer that it is not on the recipient's allow list, because the stranger lane allows 3 undelivered events per sender and the contact lane 500. the limits page says so.

3. what the box sees and keeps

sees: the recipient's signing key, the sender's key, arrival time, padded size, the fetch pattern, the publishing and fetching addresses while connected, the allow lists. keeps on disk: events until delivered or expired, cards, allow lists. never on disk: addresses, nonces, rate windows. never anywhere: content.

4. a relay only box

a box MAY serve the relay api without a directory. it needs nothing from ohan.live: the fetch verifies against to itself, and the allow list is posted by the recipient. with no directory, it accepts a publish to and from any key that passes e4. a card names it in relays; a client publishes to every relay on the recipient's card and fetches from every relay on its own (r7).

5. what changed from relay.v0

to from the encryption key to the signing key; the seal versioned; padding; the aad; the stamp; two lanes with eviction; byte caps; the signed fetch in a header with origin and nonce; delivered as a signed call instead of a timer; the allow list; the relay only box; five minutes of skew on exp and ts.