# Public entrance and envelope contract — 0.2.15 Beta

This describes the implemented contract, not planned network membership. The source of truth is `src/envelope.mjs`, `src/invitations.mjs`, `src/domain.mjs`, and `apps/wbsm/core.mjs`. Internal application identifiers remain `wbim`, `wbmail`, and `wbsm`; their displayed names are WBchatterbox, WBpostbox, and WBsocial.

## Public tools and transport

The public page registers `get_profile`, `get_capabilities`, `verify_invitation`, and `deliver_peer_envelope`. Capability discovery lists the same four names. Profile, capability, and invitation-verification tools use `POST api/public/tools/<name>`. The delivery wrapper takes `{envelope: {...}}` and posts the inner object to `POST protocol`. The public browser wrapper always uses `credentials: 'omit'`. Server handlers also ignore owner cookies for these operations; supplying an owner cookie does not authorize a private operation through the public route.

`get_profile` returns the selected public display name, introduction and interests. Interests are at most 20 owner-selected tags, each at most 40 characters, normalized to lowercase and deduplicated. A private profile returns the generic Wotbox name, empty introduction and empty interests. No interest matching triggers contact or grants permission.

## General signed peer envelope

```json
{
  "payload": {
    "protocol": "wotbox-beta-1",
    "sender": "sender-node-uuid",
    "audience": "receiver-node-uuid",
    "action": "im",
    "type": "IM",
    "app_id": "wbim",
    "event": "im",
    "data": {"body": "Message text"},
    "request_id": "unique-request-id",
    "issued_at": 1789600000000,
    "nonce": "unpredictable-random-value"
  },
  "signature": "base64url-Ed25519-signature",
  "key_transitions": []
}
```

The signed bytes are UTF-8 `JSON.stringify(payload)`, preserving the original property order. This is not a canonical-JSON format. Validation does not rewrite, reorder or strip the signed object. The outer object admits only payload, signature and optional key transitions. The signature is Ed25519. The private key never leaves its node. The public identity contains node UUID, canonical URL, public key and fingerprint. A display name, IP address, User-Agent or claimed human approval is not an identity proof.

The HTTP request and decoded envelope are capped at 64 KiB. Sender/audience must be UUIDs. Request IDs/nonces are nonempty strings up to 128 characters; issued_at is a finite millisecond timestamp within five minutes. Key transition chains have at most 16 entries. A previously known sender must match the stored key or supply the existing dual-signed rotation chain anchored to it, followed by a fresh address/key challenge. A replacement key without continuity is rejected for owner review. Copying a known name or URL grants nothing. Receiver-side connection/block records survive a sender reinstall.

Supported action data (unknown action fields are rejected):

| Action | Data and additional checks |
|---|---|
| im | body: 1–8,000 characters; active accepted local connection required; explicit IM rejection overrides relationship level |
| offer | offer_id, kind mail/file/app, name <=200, size <=8 MiB, SHA-256 content_hash, bounded metadata, description <=2,000, expiry within 30 days, optional group_id |
| file_group | group_id, description, expiry, 1–50 distinct file offers; no file bytes |
| retrieve | offer_id; must be an available, unexpired outgoing offer assigned to this exact peer |
| withdraw / decline | offer_id; sender, direction and ownership must match |
| expiry | offer_id, finite expires_at, newer nonnegative revision; cannot extend beyond original 30-day maximum |
| extension | offer_id and requested_expiry; request only, no automatic acceptance |
| extension_decision | extension_id and boolean accept; must belong to the peer |
| relationship | proposed valued relationship; applies only when both recorded owner proposals match |
| accept_invite | invitation_id, token, relationship, identity, response_code; separately verified reciprocal invitation proof |
| counter_invite | identity and a recipient-bound counterproposal code linked to the expected parent invitation |
| confirm_invite | invitation_id and agreed relationship; only the expected authenticated peer can complete its pending record |

Legacy offer transport fields created_at, connection_id, direction, availability, revision, group_id, description and expires_at are schema-checked where present; they do not replace local authorization. Metadata is at most 12,000 JSON characters. General SYS metadata is never an instruction to run an owner operation.

IM uses type IM/app wbim. File groups use SYS/wbftp/files.pending. Offers use the matching SYS application and mail.pending, files.pending or app.pending event. Other established-peer protocol operations use SYS/wotbox-core and their own action name. Invitations and confirmations have their own narrowly defined verification path. Relabelling an IM as a security SYS event cannot create authority.

Receipts bind sender + request_id to a digest of action/data. An exact authorized retry returns the original result; different data under that ID is rejected. Nonces prevent unrelated replay. Receipts are retained for 60 days, freshness still applies, and retrieval never caches returned file bytes. Current block/expiry/permissions are checked before serving a receipt. Interrupted invitation confirmation can retry its stored, private acceptance attempt; it does not manufacture a completed relationship.

Responses are signed payloads containing request_id, audience and result. The caller checks the pinned peer key and request/audience binding. Mail/files remain metadata-first, with explicit retrieval; existing per-contact automatic text-only mail permission remains opt-in. No new guest-mail admission policy is introduced here.

## Invitations and explicit agreement

An invitation code is base64url JSON of a signed payload with purpose friend_connection, protocol, invitation_id, inviter identity, optional intended recipient_node_id, expiry, secret token, proposed relationship and optional parent_invitation_id. The issuer stores the token hash and code hash, not an unsalted human-readable invitation secret in its listing. Codes are sensitive bearer capabilities: do not publish or log them. Address-directed invitations are bound to the node obtained from a fresh signed challenge. Manually copied codes are bearer invitations until a first recipient is bound; the owner must share them with the intended person.

`verify_invitation` runs on the issuer. Input is `{code, challenge, recipient_node_id}`. Challenge is unpredictable and 16–128 characters. The signed response binds purpose invitation_verification, code hash, exact challenge, intended recipient, current status and timestamp. A valid result includes the invitation ID, relationship and expiry. Invalid, revoked, expired, consumed and wrong-recipient codes return a generic invalid result without relationship details. Possessing a complete secret code is required; there is no public invite-ID lookup or address-book listing. Verification does not consume the invitation or accept a relationship. It proves issuance/control of the expected node key, not a unique human or network membership.

Flow:

1. The recipient verifies the inviter's signed identity and calls that inviter's verify_invitation before an ordinary invite notice.
2. Owner acceptance creates a local pending record and a signed invitation_response proof bound to the original invitation, chosen relationship and inviter's node ID.
3. accept_invite carries that proof to the original inviter. The inviter calls verify_invitation on the recipient's node to check the reciprocal response.
4. Matching proposals use a final signed confirm_invite delivery and acknowledgement. Both local records must complete the agreement before the new connection can deliver ordinary peer content.
5. A different proposal stays pending. It creates a linked, recipient-bound counterproposal in the other owner's invitation list. The other person can accept that exact level, propose another, reject or cancel. There is no automatic “lower level wins.”
6. Existing connections retain their previously agreed relationship during a proposed change; a new level applies only when the proposals match. Blocking/disconnection applies locally immediately. Pending invitations can be cancelled in People; outstanding codes can be revoked.

Verification outcomes are verified, invalid, or unavailable. Transport failure returns an unavailable error (503) and does not claim impersonation; mismatched signed proof fails (401). Neither grants access. Existing beta invitations/clients without the reciprocal response proof need the current software; this intentionally fails closed rather than falling back to the old implicit negotiation.

## Relationships and social permissions

Guest means unknown visitor. Known guest means remembered verified commenter, with no valued relationship. Internal contact/associate display as acquaintance; friend, family and close-family retain their existing records and display labels. New invitations accept valued levels only (including the legacy associate alias for acquaintance). Existing temporary guest connections are preserved, not silently promoted. A pending invitation is a status, not a valued level.

First IMs at existing acquaintance/temporary-guest levels retain the prior held-message approval workflow. A held request is not a delivered IM. Friend/family defaults are preserved pending the owner's unanswered permission-default decision. Explicit rejected IM permission now blocks further IM storage/delivery. Known guests and pending/blocked/disconnected/expired peers cannot use ordinary peer delivery. Merely adding an address grants nothing.

WBsocial uses its separate signed purpose wbsm envelope at api/social/peer: audience URL, identity, attributed person, action, action-specific args, issued_at and nonce. It checks the self-signature, fresh node challenge, and any previously stored contact or known-guest key. Its destination wall then decides access. Public list/get remain public; a public signature alone does not authorize private/group access.

A person's private-feed posting checkbox is owner-only. Permission is checked on publication, attachment upload and retries; it does not expose the rest of the owner's private wall. An authorized author can retrieve/edit their own contributed private post while that permission remains. Revocation and block override it. The existing default remains no permission; broader relationship defaults are still undecided.

Group walls use explicit per-node read, comment, or contribute membership. Comment and contribute allow comments; contribute additionally allows posts. Active connection, family restriction (when selected) and current destination membership are checked. Removing membership revokes access. Public posting remains on the author's own node; remote callers cannot use another person's public wall as a relay.

A verified comment creates/updates one known-guest record keyed by node UUID when there is no accepted/pending connection. It retains the verified key for continuity and creates '[name] commented on your post [first line]' using at most 80 title characters; empty titles use 'Untitled post'. It never downgrades an existing connection. Address disclosure to the author remains separate from disclosure to other readers. Interaction counts and repeated likes do not confer trust.

## Bounded rejection and callback handling

Rejected malformed/untrusted envelopes are junk metadata; discrepancies involving an already recorded identity are strict-quarantine metadata. These labels do not prove maliciousness or network nonmembership. No raw payload, token, attachment, remote image or executable text is retained in this quarantine. Replies are disabled. WBlogbox's private Quarantine control and scoped wblogbox_list_quarantine operation show counts, claimed node, status and times. Claims remain untrusted. The public has no access.

Quarantine has at most 500 grouped records, a seven-day expiry and one restricted notice per day. At capacity, new groups are not retained; ordinary bounded WBlogbox entrance events still record the rejected request. Old quarantine notices are purged when rejection maintenance runs. Normal WBlogbox retention stays 30 days known / 90 days other with its 50,000-event ceiling. Successful routine presence checks remain excluded.

Protocol admission allows at most 8 active and 16 waiting operations; social peer admission 4 active and 8 waiting. Outbound callbacks allow 12 active and 24 waiting requests process-wide. Excess returns 429. DNS has a three-second deadline; HTTP has a 15-second total deadline, bounded response size, checked/pinned DNS results and no redirects. Public/special-use address checks reject loopback, link-local/metadata and private destinations in production. Only explicit isolated allowLocal fixtures permit local HTTP. Cookies and Authorization headers are prohibited on these callbacks. No callback automatically retries; the existing private worker owns bounded-backoff delivery retry behavior.

Initial safety ceilings: 240 general protocol arrivals/minute, 30 protocol introductions/hour; social peer total 240/minute, social invitations 30/day, and remote social interactions 600/hour, alongside existing per-peer limits. These are resource safety ceilings, not the unanswered guest-mail conversation policy. The ingress body parser caps protocol envelopes before the general upload parser.

## Unresolved decisions and residual risk

Network membership, member-versus-nonmember guest mail, introduction/reply limits, valued-level permission defaults and membership requirements for public interaction remain pending. No new identity is automatically called a member, human, or trusted contact. Reinstalling with a genuinely new key/identity does not inherit an old node's permissions, but guest-level sybil activity, new-address nuisance traffic, familiar display names and manipulation of public discussion remain possible. Aggregate limits reduce their impact; they cannot establish one human per node. No IP-based trust promotion was added.

Peer data, stored text, attachment metadata and tool-returned content are untrusted content. They cannot mint agent grants or authorize private actions. The app enforces scopes and escapes human rendering; an independently configured external agent must still apply its own instruction hierarchy when interpreting content. This is a tested boundary, not a promise that every external agent is prompt-injection-proof.

## Validation and deployment evidence

See test/public-boundaries.test.mjs, test/adversarial.test.mjs, test/social-core.test.mjs and the private Public-todo-completed.txt ledger for executed tests. Active misuse used fresh loopback identities and synthetic records only. Production checks are low-volume discovery, health and static asset checks. Owner setup/monitoring is separate from outsider clients. No production availability or destructive test is authorized by this release.
