Microtoll Engine pre-1.0 · the locks, pre-built

@microtoll/mailbox — formats

Status: decided (D-40, 2026-09-25; the signature's binding reserved by D-31). The label is frozen by the crypto-core fixture file (its mailbox section, under the test namespace), which every later version must reproduce; the bundle is version 2. Version-2 bundles this package wrote under that label (an invitation, an acknowledgement and a hybrid invitation) are frozen in test/fixtures/frozen-v2.json.

1. The label

shared = ECDH-P256(myPrivateSealingKey, theirPublicSealingKey)          (256 bits)
info   = "<ns>/invite-mailbox/v2|<YYYY-MM>|" + base64url(senderPublicKey) + "|" + base64url(recipientPublicKey)
label  = HKDF-SHA-256(shared, salt ∅, info, 256 bits)                    (32 bytes)

2. The bundle (version 2)

payloadJson = JSON { kind, objectId, kObject, keyEpoch, senderName, <claims…>, senderIdentityKey, senderSigningKey }
message     = frameContext("<ns>/sig/invite/v2", label, SHA-256(recipientKey)) ‖ UTF-8(payloadJson)
sig         = Ed25519(senderSigningKey, message)
bundle      = sealToRecipient(recipientKey, UTF-8(JSON { v: 2, payloadJson, sig }))
FieldInvitation (kind: "invite")Acknowledgement (kind: "invite-ack")
objectIdthe objectthe object the link opened
kObjectbase64url of K_objectnull
keyEpochthe epoch the key is fornull
senderNameshown only when verifiedshown only when verified
hashedToken, stage—which link was used; what is reported ("seen", …)
claimsthe app's own fields, inside the signed payloadthe same

3. The server's part

send-invite { mailboxId, encryptedBundle, expiresAt } → { inviteId }; poll-invites { mailboxIds } → rows with the bundle for uncollected drops and consumed: true without it for collected or withdrawn ones; consume-invite { inviteId } (collection and withdrawal alike; the row's bundle is emptied, so a collected or withdrawn drop keeps no key); watch-invites { mailboxIds } and the invite-live { mailboxId } push. All in @microtoll/blind-store.