master xplshn/wire-docs / docs / wire_spec-0.8.txt
   1  ██╗    ██╗██╗██████╗ ███████╗
   2  ██║    ██║██║██╔══██╗██╔════╝
   3  ██║ █╗ ██║██║██████╔╝█████╗
   4  ██║███╗██║██║██╔══██╗██╔══╝
   5  ╚███╔███╔╝██║██║  ██║███████╗
   6   ╚══╝╚══╝ ╚═╝╚═╝  ╚═╝╚══════╝
   7  wire protocol specification
   8  rev %-version-%
   9  author: xplshn / wire-wg
  10  license: ISC
  11
  12  revision history: see CHANGES.md
  13  implementation notes (Go): see IMPL.md
  14
  15%- version = "0.8" -%
  16
  17========================================================================
  18WIRE - a sane chat protocol
  19========================================================================
  20
  21  WIRE is a text-based, line-oriented, chat protocol encoded in UTF-8.
  22  It replaces idioms found in IRC and Discord with a set of orthogonal
  23  primitives.  It is not backwards compatible with IRC or any
  24  derivative.
  25
  26  The WIRE protocol is governed by five core design principles:
  27
  28  1.  Orthogonality -- Protocol primitives are independent and
  29      composable, with minimal semantic overlap.
  30  2.  Transparency -- All protocol state is explicit; no implicit or
  31      hidden context is assumed.
  32  3.  Extensibility -- Features are introduced via optional namespaces
  33      and capabilities.
  34  4.  Efficiency -- Each logical operation is represented by exactly
  35      one line.
  36  5.  Human Legibility -- The protocol is directly readable and
  37      writable by humans.  Minimal ("L0") clients, bots, and simple
  38      text tools are first-class participants.
  39
  40  Non-goals:
  41
  42  - Compatibility with IRC or IRC-derived protocols
  43  - Mandatory encryption at the protocol level (transport security is
  44    provided by TLS; optional end-to-end encryption is defined in the
  45    e2e capability, see %-history_purge_e2ee-%)
  46  - Binary framing
  47  - Uniform feature support across all clients
  48
  49  The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
  50  "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
  51  document are to be interpreted as described in RFC 2119.
  52
  53%- transport = "Sect.  1" -%
  54========================================================================
  55%-transport -% -- TRANSPORT
  56========================================================================
  57
  58  WIRE operates over TCP.  TLS is REQUIRED for all non-loopback
  59  connections and MUST be version 1.3 or higher.  Plain TCP MAY be used
  60  only on loopback interfaces.  The default port is 6789.
  61
  62  %- transport_encodingandframing = "Sect.  1.1" -%
  63  1.1  Encoding and Framing
  64
  65    All data MUST be encoded in UTF-8.  The protocol is line-delimited;
  66    each line is a single message terminated by a single Line Feed (LF,
  67    %x0A) octet.
  68
  69    A Carriage Return (CR, %x0D) octet immediately preceding LF MUST be
  70    ignored by receivers.  A CR octet appearing in any other position is
  71    invalid and constitutes a protocol error.
  72
  73  %- transport_errorhandling = "Sect.  1.2" -%
  74  1.2  Error Handling
  75
  76    On invalid CR placement, the server MUST send ERR code=BADARG and
  77    close the connection.
  78
  79  %- transport_linelengthlimits = "Sect.  1.3" -%
  80  1.3  Line Length Limits
  81
  82    The maximum line length is 4096 octets, measured on the wire after
  83    UTF-8 encoding and before escape processing.  This includes verb,
  84    tags, optional " :", payload, and LF.
  85
  86  %- transport_enforcement = "Sect.  1.4" -%
  87  1.4  Enforcement
  88
  89    Overlong lines MUST be rejected with ERR code=TOOLONG.  If sent by a
  90    client, the server MUST close the connection, as framing can no
  91    longer be trusted.
  92
  93%- lineformat = "Sect.  2" -%
  94========================================================================
  95%-lineformat-% -- LINE FORMAT
  96========================================================================
  97
  98  %- lineformat_overview = "Sect.  2.1" -%
  99  2.1  Overview
 100
 101    WIRE messages are UTF-8 encoded sequences terminated by a single
 102    line feed (LF).  Each message consists of a mandatory verb, followed
 103    by zero or more metadata tags, and an optional payload.
 104
 105  %- lineformat_grammar = "Sect.  2.2" -%
 106  2.2  Grammar
 107
 108    The syntax is defined using ABNF as specified in RFC 5234:
 109
 110      line        = verb *(SP tag) [SP ":" payload] LF
 111      verb        = 1*16UPPER
 112      tag         = tag-key "=" tag-val
 113      tag-key     = ALPHA *31( ALPHA / DIGIT / "_" )
 114      tag-val     = 1*(TCHAR / pct-encoded)
 115      pct-encoded = "%" 2HEXDIG
 116      payload     = *PCHAR
 117
 118      UPPER       = %41-5A
 119
 120      TCHAR       =  %x01-08 / %x0B-0C / %x0E-1F
 121      TCHAR       =/ %x21-3C / %x3E-7E / %x80-FF
 122                  ; any octet except
 123                  ;   NUL %x00 TAB   %x09 LF  %x0A
 124                  ;   CR  %x0D SPACE %x20 '%' %x25 '=' %x3D
 125
 126      PCHAR       = %x01-09 / %x0B-0C / %x0E-FF
 127                  ; any octet except NUL %x00 and LF %x0A
 128
 129    The verb is a case-sensitive uppercase ASCII string between one and
 130    sixteen octets in length.  Tags are key-value pairs separated from
 131    the verb and from each other by a single space (SP).
 132
 133  %- lineformat_payloadsemantics = "Sect.  2.3" -%
 134  2.3  Message Delimitation and Payload Semantics
 135
 136    The payload, if present, is introduced by the first occurrence of
 137    the two-octet sequence SP ":" (%x20 %x3A).  Because the SP octet
 138    cannot appear within the verb or within unencoded tag values, this
 139    sequence unambiguously separates the head from the payload.
 140
 141    The colon is a structural delimiter and MUST NOT be included in the
 142    interpreted payload.  However, a payload can have begin with ":",
 143    i.e. the line "... ::text" has the payload ":text".
 144
 145    A message without the SP ":" delimiter has no payload.  A message in
 146    which the delimiter is immediately followed by LF has a present but
 147    zero-length payload.  Implementations MUST distinguish between these
 148    cases.
 149
 150  %- lineformat_tagsemantics = "Sect.  2.4" -%
 151  2.4  Tag Semantics
 152
 153    Tags are unordered metadata elements.  Receivers MUST ignore
 154    unrecognized tag keys.
 155
 156    If a tag key appears multiple times within a single line, only the
 157    first occurrence is significant.  Subsequent occurrences MUST be
 158    ignored and MUST NOT be treated as an error.
 159
 160    For response lines (e.g., OK), servers MUST include all tags
 161    required by the response grammar.  Tags not recognized by the server
 162    MUST be silently discarded and MUST NOT be echoed.  This requirement
 163    does not apply to server-originated broadcast or asynchronous lines.
 164
 165  %- lineformat_tagvalueencoding = "Sect.  2.5" -%
 166  2.5  Tag Value Encoding
 167
 168    Tag values are restricted to the TCHAR production and the
 169    percent-encoding mechanism defined in this section.  The octets NUL,
 170    TAB, LF, CR, SP, and = (%x00, %x09, %x0A, %x0D, %x20, and %x3D) MUST
 171    NOT appear in unencoded form.
 172
 173    Any disallowed octet or any octet not representable by TCHAR MUST be
 174    encoded using percent-encoding as defined in RFC 3986, sec. 2.1.
 175    Each encoded octet is represented as a percent character followed by
 176    exactly two uppercase hexadecimal digits.
 177
 178    The percent character (%x25) MUST be encoded as %25 whenever
 179    encoding is applied.  A literal percent character is permitted only
 180    when it begins a valid percent-encoded sequence.
 181
 182    Receivers MUST decode all valid percent-encoded sequences before
 183    interpreting tag values.  Any malformed encoding, including invalid
 184    hexadecimal digits or truncated sequences, MUST result in an ERR
 185    message with code BADARG.
 186
 187    Characters in the unreserved set [A-Za-z0-9.~_-] SHOULD NOT be
 188    encoded, but MAY be encoded.  Receivers MUST accept both forms.
 189
 190    Servers MUST relay tag values verbatim and MUST NOT perform
 191    decoding, re-encoding, or normalization when forwarding messages.
 192    Decoding MAY be performed internally when required for processing.
 193
 194  %- lineformat_payloadencoding = "Sect.  2.6" -%
 195  2.6  Payload Encoding and Escape Sequences
 196
 197    The payload is an arbitrary sequence of octets subject to the
 198    constraints defined in this section.  The NUL and LF octets (%x00
 199    and %x0A) MUST NOT appear in the payload.
 200
 201    The presence of a literal LF octet within the payload region
 202    constitutes a framing violation.  In such cases, the server MUST
 203    respond with ERR code=BADARG and MUST close the connection.
 204
 205    After escape processing, the decoded payload length MUST NOT exceed
 206    4096 octets.  If a MSG payload exceeds this limit, the server MUST
 207    reject it with ERR code=TOOLONG.  Escape processing uses a
 208    single-pass, non-recursive mechanism in which the backslash (%x5C)
 209    is the escape introducer.  The sequences \n, \t, and \\ represent
 210    line feed, horizontal tab, and a literal backslash, respectively.
 211
 212    Processing MUST be performed left-to-right in a single pass.  The
 213    sequence \\n decodes to a backslash followed by "n" and MUST NOT be
 214    interpreted as a line feed.
 215
 216    Any escape sequence not defined in this section MUST be preserved
 217    verbatim.  Clients MUST NOT treat unknown escape sequences as
 218    errors.
 219
 220    Escape processing is a client-side concern.  Servers MUST relay
 221    payloads verbatim and MUST NOT interpret, normalize, or otherwise
 222    modify escape sequences during transit.
 223
 224    If a payload contains no backslash octets, escape processing is a
 225    no-op; in such instances, implementations SHOULD avoid unnecessary
 226    memory allocation or redundant scanning to ensure processing
 227    efficiency.
 228
 229  %- lineformat_examples = "Sect.  2.7" -%
 230  2.7  Examples
 231
 232    MSG cid=01JXYZ456 mid=01JXYZ789 :hello world
 233    SUB rid=/wire.example.org/acme/general
 234    ERR code=NOPERM :you are not allowed to do that
 235    REACT mid=01JXYZ789 eid=thumbsup
 236    PING
 237    MSG cid=01JXYZ456 :line one\nline two
 238
 239
 240  %- lineformat_parsingmodel = "Sect.  2.8" -%
 241  2.8  Parsing Model
 242
 243    This section defines the normative parsing algorithm for a WIRE
 244    message.  Implementations MAY utilize any internal strategy,
 245    provided the externally observable behavior is identical to the
 246    procedure described herein.
 247
 248    Given an input line:
 249
 250      1.  Framing and Sanitization
 251
 252          A line MUST terminate with LF (%x0A).  If the octet
 253          immediately preceding LF is CR (%x0D), it MUST be discarded.
 254          Any other occurrence of CR within the line MUST be treated as
 255          a protocol error.
 256
 257      2.  Length Validation
 258
 259          After removal of an optional trailing CR and excluding the
 260          terminating LF, the line length MUST NOT exceed 4096 octets.
 261          Violations MUST result in ERR code=TOOLONG, and the server
 262          MUST close the connection.
 263
 264      3.  Payload Separation
 265
 266          The parser identifies the first occurrence of the sequence SP
 267          ":".  If present, all octets following the colon up to the
 268          terminating LF constitute the payload, and the preceding
 269          portion constitutes the head.  If absent, the entire line is
 270          the head and no payload is present.
 271
 272          The delimiter is not included in either component.
 273
 274      4.  Head Processing
 275
 276          The head MUST be split on SP octets.  The first token is the
 277          verb and MUST conform to the verb production.  Remaining
 278          tokens are tag expressions.  Failure to parse a valid verb or
 279          malformed tokens MUST result in ERR code=BADARG.
 280
 281      5.  Tag Processing
 282
 283          Each tag token MUST contain exactly one '=' octet separating
 284          the key and value.  Duplicate keys are handled according to
 285          the rule defined in %-lineformat_tagsemantics-%.
 286
 287          Tag values MUST be validated and percent-decoded.  Any
 288          malformed encoding MUST result in ERR code=BADARG.
 289
 290      6.  Payload Handling
 291
 292          If present, the payload MUST be treated as an uninterpreted
 293          octet sequence.  Escape processing, as defined in
 294          %-lineformat_payloadencoding-%, is performed by clients only.
 295
 296      7.  Error Handling
 297
 298          Parsing is defined as a single-pass process.  Upon any
 299          validation failure, the server MUST emit an ERR response with
 300          an appropriate code.  The connection MUST be terminated if the
 301          error compromises framing or stream integrity.
 302
 303  %- lineformat_responserouting = "Sect.  2.9" -%
 304  2.9  Response Routing
 305
 306    %- lineformat_responserouting_generalrule = "Sect.  2.9.1" -%
 307    2.9.1  General Rule
 308
 309      Certain verbs are used for both single-item queries and multi-item
 310      list responses.  These include, but are not limited to, CHAN,
 311      ROLE, MEMBER, COMM, VOICE, THREAD, SRVADM, TYPING, INVITE, PIN,
 312      KVAL, PROFILE, PUBKEY, UPLOAD, and ATTACH.
 313
 314      Clients MUST route response lines using pending-request state.
 315      Each response line is associated with the most recently issued
 316      request that has not yet received its terminal OK.  This rule is
 317      universal; implementations MUST NOT introduce per-verb special
 318      cases.
 319
 320    %- lineformat_singleline_multiline_responses = "Sect.  2.9.2" -%
 321    2.9.2  Single-Line and Multi-Line Responses
 322
 323      The verbs described above MAY produce either a single response
 324      line or a sequence of response lines.  In all cases, the response
 325      is terminated by a final OK line.
 326
 327      Clients MUST continue consuming response lines until the terminal
 328      OK is received and MUST NOT assume that a single response line
 329      constitutes a complete response.
 330
 331      When present, the count= tag in the terminal OK indicates the
 332      number of response lines and MAY be used to validate completeness.
 333
 334    %- lineformat_subunsub = "Sect.  2.9.3" -%
 335    2.9.3  SUB and UNSUB
 336
 337      The SUB and UNSUB verbs each produce exactly one terminal OK and
 338      otherwise follow the same pending-request routing rules as all
 339      other requests.
 340
 341    %- lineformat_pipelining = "Sect.  2.9.4" -%
 342    2.9.4  Pipelining
 343
 344      Clients MUST NOT issue concurrent requests targeting the same
 345      resource.  Two requests are considered to target the same resource
 346      if they use the same verb and identical identifying parameters
 347      (e.g., cid= or gid=).  Such requests are indistinguishable at the
 348      protocol level and cannot be reliably routed.
 349
 350      Clients MAY pipeline requests that target distinct resources.  In
 351      such cases, response lines for multi-line verbs typically include
 352      sufficient identifying tags (e.g., cid= or gid=) to permit routing
 353      based on tag inspection.  The pending-request rule serves as a
 354      fallback when tag-based routing is insufficient.
 355
 356      Server-generated broadcast events, including MSG, EDIT, DEL,
 357      REACT, UNREACT, PIN, UNPIN, and history-related events, include
 358      identifying tags such as cid= and are therefore self-identifying.
 359      Clients MAY route such events independently of pending-request
 360      state.
 361
 362    %- lineformat_readstateexception = "Sect.  2.9.5" -%
 363    2.9.5  READSTATE Exception
 364
 365      Clients MUST NOT pipeline the requests READSTATE op=query
 366      cid=<cid> and READSTATE op=query gid=<gid> when the specified
 367      group contains the referenced channel.  Both forms produce
 368      response lines of the form READSTATE op=query cid=<cid> ..., which
 369      are indistinguishable based solely on their content.
 370
 371      Accordingly, clients MUST serialize these requests.  This is the
 372      only cross-form pipelining restriction defined by the protocol;
 373      all other restrictions apply only to requests targeting identical
 374      resources.
 375
 376    %- lineformat_directionaldisambiguation = "Sect.  2.9.6" -%
 377    2.9.6  Directional Disambiguation
 378
 379      Response routing applies only to server-generated response lines.
 380      It is distinct from bidirectional verb disambiguation (see
 381      %-lineformat_bidirectionalverbs-%), where the same verb may appear
 382      in both client-to-server and server-to-client directions.
 383
 384      In such cases, direction is determined by tag content, most
 385      commonly the op= value, and in some cases by the presence of
 386      server-specific tags such as uid= or ts=.
 387
 388      Response routing MUST NOT be used to disambiguate bidirectional
 389      verbs.
 390
 391  %- lineformat_protocolinvariants = "Sect.  2.10" -%
 392  2.10  Protocol Invariants
 393
 394    The requirements in this section apply to all protocol operations.
 395
 396    %- lineformat_commitbeforeok = "Sect.  2.10.1" -%
 397    2.10.1  Commit-Before-OK
 398
 399      For any mutating request that results in an OK response, the
 400      server MUST commit the corresponding state change to its
 401      authoritative store before transmitting the OK.
 402
 403      A client issuing a subsequent query after receiving OK MUST
 404      observe the committed state.
 405
 406      The UPLOAD operation is an exception.  In this case, the OK
 407      response confirms only upload slot negotiation; the actual commit
 408      occurs asynchronously after the associated HTTP transfer
 409      completes.
 410
 411    %- lineformat_listshaperules = "Sect.  2.10.2" -%
 412    2.10.2  Shape of List Responses
 413
 414      A terminal OK concluding a multi-line response MUST include both
 415      count= and total= tags.  The count= value indicates the number of
 416      response lines in the current page, while total= indicates the
 417      total number of matching items across all pages.  For bounded
 418      result sets, these values are equal.
 419
 420      Certain operations deviate from this model.  History-oriented
 421      operations such as HIST and LIST use a has_more= indicator instead
 422      of total= due to pagination over unbounded, mutating streams.  The
 423      READSTATE op=query operation returns a mapping from channel
 424      identifiers to state values; in this case, count= appears on
 425      individual response lines, and the terminal OK omits total=. These
 426      exceptions are specific and do not establish a general pattern.
 427
 428    %- lineformat_timestampsemantics = "Sect.  2.10.3" -%
 429    2.10.3  Timestamp Semantics
 430
 431      In all op=info response lines, the ts= tag represents the Unix
 432      timestamp, in milliseconds, of the most recent mutation to the
 433      entity.
 434
 435      In all broadcast event lines, ts= represents the time at which the
 436      server committed the event.
 437
 438      This interpretation MUST be preserved unless explicitly overridden
 439      by the relevant section.  For example, VOICE op=info uses the
 440      joined= tag to represent session join time, consistent with MEMBER
 441      op=info.
 442
 443  %- lineformat_bidirectionalverbs = "Sect.  2.11" -%
 444  2.11  Bidirectional Verb Directionality
 445
 446    Some verbs may be transmitted in both client-to-server and
 447    server-to-client directions.  The direction of a line is determined
 448    by the presence of the uid= tag.
 449
 450    Client-generated lines MUST NOT include uid=.  Server-generated
 451    lines MUST include both uid= and ts=.  A line containing uid= is
 452    therefore considered server-originated, while a line without uid= is
 453    considered client-originated.
 454
 455    Any violation of this rule MUST be treated as a protocol error.
 456
 457    This rule applies to verbs including THREAD, PIN, EMOJI, KVAL,
 458    VSTREAM, and VOICE, though it is not limited to these.
 459
 460  %- lineformat_paginatedops = "Sect.  2.12" -%
 461  2.12  Paginated Operations
 462
 463    List operations follow a uniform pagination model based on offset=
 464    and limit= parameters.
 465
 466    A list request is expressed as VERB op=list with optional offset=
 467    and limit= parameters.  If omitted, offset= defaults to zero and
 468    limit= defaults to a verb-specific value.  Servers MUST enforce a
 469    maximum limit and clamp any larger value accordingly.
 470
 471    The response consists of zero or more VERB op=info lines, followed
 472    by a terminal OK line containing count= and total=.
 473
 474    The count= value indicates the number of items returned in the
 475    current page, while total= indicates the total number of matching
 476    items.
 477
 478    Default and maximum limits are defined per operation.  For example,
 479    history and subscription-related operations typically default to 50
 480    items with a maximum of 200, while community listing operations use
 481    lower limits.  Individual operation sections define the exact
 482    values.
 483
 484    History-oriented operations and other unbounded streams use the
 485    has_more= tag instead of total= as described in
 486    %-lineformat_listshaperules-%.
 487
 488%- identifiers = "Sect.  3" -%
 489========================================================================
 490%-identifiers-% -- IDENTIFIERS
 491========================================================================
 492
 493  %- identifiers_overview = "Sect.  3.1" -%
 494  3.1  Overview
 495
 496    WIRE defines identifiers for all addressable entities.  With the
 497    exception of User Identifiers (uid) and Resource Paths (rid), all
 498    identifiers are 26-character ULIDs (Universally Unique
 499    Lexicographically Sortable Identifiers).
 500
 501    A standard ULID within WIRE consists of a 48-bit timestamp coupled
 502    with an 80-bit randomness component, which is then encoded using
 503    Crockford Base32.  This structure ensures that identifiers are
 504    lexicographically sortable by their creation time while providing
 505    strong collision resistance across distributed server instances.
 506
 507    The following identifier types are defined:
 508
 509      uid   -- User: Derived from public key, see %-identifiers_uid-%
 510      cid   -- Channel: Any addressable room
 511      sid   -- Server: Instance identifier
 512      mid   -- Message: Specific message instance
 513      eid   -- Emoji: Custom emoji identifier
 514      aid   -- Attachment: File/media attachment
 515      tid   -- Thread: Conversation thread
 516      gid   -- Group: Community or group identifier
 517      rolid -- Role: Permission/role identifier
 518
 519    Resource Paths (rid), which serve as human-readable, hierarchical
 520    strings for deep-linking, intentionally diverge from the ULID
 521    format.  Their construction and constraints are detailed in
 522    %-identifiers_ulid-%.
 523
 524  %- identifiers_uid = "Sect.  3.2" -%
 525  3.2  User Identifiers (uid)
 526
 527    %- identifiers_uid_overview = "Sect.  3.2.1" -%
 528    3.2.1  Overview
 529
 530      The User Identifier (uid) serves as an identity-derived, globally
 531      stable reference for a user.  Unlike resource identifiers, the uid
 532      is computed deterministically from the user’s cryptographic public
 533      key and is never assigned by the server.
 534
 535      Clients MUST NOT supply a uid explicitly during resource creation
 536      or authentication initialization.  Instead, the server acts as the
 537      authoritative derivation engine, generating the uid from the
 538      public key presented during the authentication handshake.  Servers
 539      MUST verify that any client-claimed uid perfectly matches the
 540      derived value; mismatches MUST result in immediate rejection.
 541
 542    %- identifiers_uid_derivation = "Sect.  3.2.2" -%
 543    3.2.2  Derivation Algorithm (Ed25519):
 544
 545      The uid is computed as:
 546
 547        1.  Obtain the raw 32-byte Ed25519 public key.
 548        2.  Compute the SHA-256 digest of the key-type discriminator
 549            concatenated with the raw public key bytes:
 550              SHA-256("ed25519:" || pubkey_bytes).
 551        3.  Extract the first 16 bytes of the resulting hash.
 552        4.  Encode these 16 bytes using Crockford Base32 (uppercase,
 553            strictly without padding).
 554
 555      This deterministic process yields a fixed 26-character string. The
 556      public key MUST be exactly 32 bytes after decoding.
 557
 558      The following is a non-normative reference implementation in Go:
 559
 560        package main
 561
 562        import (
 563            "crypto/ed25519"
 564            "crypto/sha256"
 565            "encoding/base32"
 566        )
 567
 568        var alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
 569        var uidEncoding = base32.NewEncoding(alphabet).
 570            WithPadding(base32.NoPadding)
 571        var prefix = []byte("ed25519:")
 572
 573        func UIDFromPublicKey(pub ed25519.PublicKey) string {
 574            data := append(prefix, pub...)
 575            sum := sha256.Sum256(data)
 576            return uidEncoding.EncodeToString(data[:16])
 577        }
 578
 579    %- identifiers_uid_properties = "Sect.  3.2.3" -%
 580    3.2.3  Properties
 581
 582      -  Consistency: Identical public keys MUST reliably yield
 583         identical uid strings across all server implementations.
 584
 585      -  Collision Resistance: The derivation algorithm utilizes a
 586         128-bit output space, providing cryptographically strong
 587         resistance against collisions.
 588
 589      -  Format Constraints: The uid is formatted as a 26-character
 590         Crockford Base32 string.  While it visually matches the
 591         character set of a standard ULID, it explicitly does not encode
 592         a timestamp and is therefore not time-ordered.
 593
 594      -  Validation: Servers MUST independently derive the uid from the
 595         presented public key and MUST reject the authentication request
 596         if the supplied uid fails to match the server-derived value.
 597
 598    %- identifiers_uid_keytypediscriminators = "Sect.  3.2.4" -%
 599    3.2.4  Key Type Discriminators
 600
 601      To prevent confusion between algorithms and ensure future-proof
 602      key handling, the uid derivation MUST include a key-type
 603      discriminator prefix:
 604
 605        uid = base32(SHA256(discriminator || pubkey_bytes)[:16])
 606
 607      In WIRE 0.8, the only defined key-type discriminator is
 608      "ed25519:", which indicates an Ed25519 public key.
 609
 610      Discriminators MUST be unique and non-overlapping.  Any
 611      specification introducing a new cryptographic key type must define
 612      a distinct discriminator prior to implementation. Since the uid is
 613      computed directly from the discriminator and the key, changing the
 614      underlying key type inherently produces a new uid.
 615
 616      Clients that need to upgrade or modify their cryptography MUST
 617      utilize the KEYROTATE mechanism (see
 618      %-authentication_keyrotate_overview-%) to preserve identity
 619      continuity.
 620
 621    %- identifiers_uid_keyfingerprint = "Sect.  3.2.5" -%
 622    3.2.5  Key Fingerprint
 623
 624      For the purposes of out-of-band human verification, clients SHOULD
 625      generate and display a visual key fingerprint.  This fingerprint
 626      is derived from the SHA-256 hash of the public key, formatted as
 627      lowercase hexadecimal characters, and grouped into 8-byte (16 hex
 628      character) colon-separated segments.
 629
 630        aabbccdd11223344:eeff00112233aabb:ccddeeff00112233:aabb99887766
 631
 632      This fingerprint is strictly intended for local UI display and
 633      user-to-user verification; it is not transmitted over the WIRE
 634      protocol.
 635
 636  %- identifiers_ulid = "Sect.  3.3" -%
 637  3.3  ULID-Based Identifiers
 638
 639    %- identifiers_ulid_generation = "Sect.  3.3.1" -%
 640    3.3.1 Generation
 641
 642      All ULID identifiers are server-generated.
 643
 644      Message, attachment, emoji, and thread identifiers (mid, aid, eid,
 645      tid) MUST be minted at the exact moment the corresponding resource
 646      is created.
 647
 648      Structural identifiers (cid, gid, rolid) MUST be assigned by the
 649      server when the parent entity is instantiated.
 650
 651     The server is solely responsible for ensuring that all identifiers
 652     are unique within their respective scope.
 653
 654    %- identifiers_ulid_clientconstraints = "Sect.  3.3.2" -%
 655    3.3.2  Client Constraints
 656
 657      Clients MUST NOT provide precomputed values for any
 658      server-assigned identifier when creating a resource. If a client
 659      does supply a value for a server-assigned field, the server MUST
 660      reject the request, returning an ERR response with code=BADARG.
 661
 662    %- identifiers_ulid_monotonicity = "Sect.  3.3.3" -%
 663    3.3.3  Monotonicity
 664
 665      To guarantee strictly deterministic chronological ordering,
 666      servers must enforce monotonicity. When generating multiple ULIDs
 667      of the same type within a single millisecond, the randomness
 668      component of each subsequent ULID must be strictly greater than
 669      that of the previous one. This ensures correct per-channel message
 670      ordering even under high-concurrency conditions (see
 671      %-messages_ordering-%).
 672
 673      If the randomness component exhausts its 80-bit capacity within a
 674      single millisecond, the server must increment the timestamp by at
 675      least one millisecond before generating additional ULIDs.
 676
 677  %- identifiers_scopeanduniqueness = "Sect.  3.4" -%
 678  3.4  Identifier Scope and Uniqueness
 679
 680    %- identifiers_uniqueness_mid = "Sect.  3.4.1" -%
 681    3.4.1  Message Identifiers (mid)
 682
 683      Message identifiers are strictly scoped to their parent channel. A
 684      mid MUST be guaranteed unique within the boundaries of a specific
 685      channel.
 686
 687      Because ULID randomness is finite, collisions across entirely
 688      different channels are statistically possible.  Therefore, clients
 689      MUST NOT treat mid values as globally unique primary keys.  All
 690      client-side storage, caching, and state management keyed by a
 691      message MUST utilize a composite key consisting of both the
 692      channel and the message: (cid, mid).
 693
 694  %- identifiers_rid = "Sect.  3.5" -%
 695  3.5  Resource Paths (rid)
 696
 697      A Resource Path (rid) is a stable, hierarchical, slash-separated
 698      string primarily utilized for logical navigation and deep-linking.
 699      These URL-friendly identifiers are designed to expose the logical
 700      topology of the system to end-users.
 701
 702      To be considered valid, a rid MUST begin with a leading forward
 703      slash (/).  Every segment within the path MUST be constrained to
 704      lowercase alphabetic characters, numeric digits, hyphens, and
 705      underscores.  Paths MUST NOT contain empty segments (e.g., //) and
 706      MUST NOT terminate with a trailing slash.
 707
 708      To prevent abuse and maintain performance, paths are strictly
 709      limited to a maximum depth of eight segments, with each individual
 710      segment capped at 64 characters in length.
 711
 712        Example: /server-slug/community-slug/channel-slug
 713
 714      The following ABNF defines the normative structure for a valid
 715      rid:
 716
 717        rid     = 1*8( "/" segment )
 718        segment = 1*64( SEGCHAR )
 719        SEGCHAR = %x61-7A / %x30-39 / "-" / "_"  ; [a-z0-9_-]
 720
 721      In addition to the standard slash-separated form, the WIRE
 722      protocol recognizes two convenient shorthand forms when a rid is
 723      utilized as a subscription (SUB) target (see
 724      %-namespaces_resources-%):
 725
 726      @uid
 727          Resolves to a Direct Message (DM) stream with the specified
 728          user.
 729      #cid
 730          Resolves to a specific channel identified by its opaque ULID.
 731
 732      These shorthand expressions do not conform to the normative rid
 733      ABNF productions.  Consequently, the server MUST intercept and
 734      resolve these shortcuts into their fully qualified, canonical rid
 735      counterparts prior to core processing.  The @uid form is
 736      intrinsically local to the current server instance.  The #cid form
 737      is similarly utilized within message markup (%-wire_markup_2-%)
 738      and does not function as a standard rid in that specific context.
 739
 740  %- identifiers_slugs = "Sect.  3.6" -%
 741  3.6  Slugs
 742
 743    A slug is a highly stable, human-readable string that functions as a
 744    single segment within a Resource Path (rid).  Slugs act as the
 745    immutable logical aliases for underlying resources.  This
 746    immutability ensures that deep-links and external bookmarks remain
 747    perpetually functional, even if a community or channel administrator
 748    later modifies the user-facing display name of the resource.
 749
 750    The allowable character set for a slug is a strict subset of the
 751    segment rule, but constrained to a tighter maximum length to
 752    prioritize URL brevity.  The normative ABNF structure for a slug is:
 753
 754      slug     = 1*32( SLUGCHAR )
 755      SLUGCHAR = %x61-7A / DIGIT / "-" / "_"  ; [a-z0-9_-]
 756
 757    Slug uniqueness is enforced within specific logical boundaries to
 758    preclude routing collisions:
 759
 760      -  Community slugs: MUST be unique globally across a single server
 761         instance.
 762      -  Channel slugs: MUST be unique within their parent community.
 763
 764    Slugs are strictly immutable the moment a resource is committed to
 765    the server’s authoritative datastore.  Alterations to a resource’s
 766    human-friendly display name MUST NOT trigger a change to the
 767    underlying slug.  If an administrator wishes to force a transition
 768    to a new slug, the underlying entity MUST be entirely deleted and
 769    recreated.  This unyielding constraint guarantees that rid
 770    references are safe for long-term external indexing.
 771
 772    If a client requests resource creation but omits the slug parameter,
 773    the server MUST autonomously derive the slug via the following
 774    ordered algorithm:
 775
 776      1.  Normalize the provided display name using Unicode NFC, then
 777          convert it to lowercase.
 778      2.  Iterate through the string, replacing any character not
 779          present in the SEGCHAR set with a hyphen (-).
 780      3.  Collapse any consecutive hyphens into a single hyphen.
 781      4.  Trim trailing and leading hyphens from the string.
 782      5.  Truncate the resulting string to a maximum of 32 characters.
 783      6.  If the result is completely empty, or if it collides with a
 784          pre-existing slug within the same scope, the server MUST
 785          append a decimal numeric suffix (e.g., -2, -3) without leading
 786          zeros, iterating until a globally unique slug is successfully
 787          produced.
 788
 789    Clients MUST NOT attempt to predict the final slug.  The
 790    authoritative server-generated slug, returned to the client in the
 791    terminal OK response, is the sole valid value for constructing
 792    subsequent rid references.
 793
 794%- authentication = "Sect.  4" -%
 795========================================================================
 796%-authentication-% -- AUTHENTICATION
 797========================================================================
 798
 799  In the WIRE protocol, the cryptographic keypair intrinsically
 800  represents the identity.  The uid is a deterministic artifact derived
 801  directly from a user’s public key (see %-identifiers-%). The system
 802  strictly eschews the traditional concept of "accounts" or discrete
 803  registration phases independent of the authentication flow; a user’s
 804  initial successful cryptographic handshake with a server implicitly
 805  establishes their presence and provisions their identity.
 806
 807  WIRE supports two primary authentication vectors:
 808
 809    -  PUBKEY
 810
 811       The foundational and primary authentication method.  It relies on
 812       an Ed25519 keypair, where the uid is derived deterministically
 813       from the public key.  Any entity possessing the corresponding
 814       private key can cryptographically prove their authorization to
 815       authenticate as that uid.
 816
 817    -  TOKEN
 818
 819       A secondary, highly restricted authentication method. It utilizes
 820       an opaque bearer token issued by a server on behalf of a specific
 821       uid.  This method is expressly designed for headless bots, API
 822       access, and transient session tokens where exposing the private
 823       key to local memory is undesirable or impossible.  A TOKEN is
 824       permanently bound to a specific uid and scoped strictly to the
 825       issuing server instance.
 826
 827  %- authentication_connectionprelude = "Sect.  4.1" -%
 828  4.1  Connection Prelude
 829
 830    The initialization of a WIRE connection requires a strict preamble
 831    to exchange protocol versions and establish baseline server state
 832    before authentication can proceed.
 833
 834      CLIENT                               SERVER
 835      ──────                               ──────
 836      HELLO version=1 [level=N]
 837            :clientname/version   ────►
 838                                   ◄────   HELLO sid=<ulid> ts=<unix-ms>
 839                                                :servername/version
 840      [CAPS]                      ────►
 841                                   ◄────   [CAPS list=csv]
 842
 843    The server responds to the initial HELLO with its sid (a server ULID
 844    that remains strictly stable across service restarts) and ts (the
 845    server’s current internal Unix timestamp in milliseconds).  Clients
 846    SHOULD record this ts value, as it serves as a mandatory component
 847    of the subsequent cryptographic challenge payload (see
 848    %-authentication_pubkey_auth-%).
 849
 850    The payload of the HELLO message MUST conform to the following ABNF
 851    structure:
 852
 853      payload = name "/" version
 854      name    = 1*63( ALPHA / DIGIT / "." / "-" / "_" )
 855      version = 1*31 ( ALPHA / DIGIT / "." / "-" )
 856
 857    Example: ":wire.example.org/0.8"
 858
 859    The server’s name provided in the HELLO payload dictates the
 860    canonical hostname for rid construction.  Clients MUST extract the
 861    substring preceding the first forward slash (/) and utilize it for
 862    all subsequent rid derivations.  In the preceding example,
 863    wire.example.org acts as the canonical hostname.
 864
 865    Servers MUST reject malformed HELLO payloads by issuing an ERR
 866    code=BADARG response, followed immediately by closing the transport
 867    connection.
 868
 869  %- authentication_pubkey_auth = "Sect.  4.2" -%
 870  4.2  PUBKEY Authentication (Ed25519)
 871
 872    The primary authentication vector utilizes a cryptographic
 873    challenge-response mechanism.  This ensures verification of private
 874    key possession without the transmission of sensitive material over
 875    the transport layer.
 876
 877    %- authentication_pubkey_overview = "Sect.  4.2.1" -%
 878    4.2.1  Protocol Exchange Overview
 879
 880      CLIENT                        SERVER
 881      ──────                        ──────
 882      AUTH method=PUBKEY
 883          uid=<uid>
 884          :b64url-pubkey    ────►
 885                             ◄────  CHALLENGE nonce=<hex32>
 886      AUTH method=PUBKEY
 887          sig=<b64url-sig>  ────►
 888                             ◄────  OK uid=<uid> :welcome
 889                                    (or ERR code=AUTHFAIL :reason)
 890
 891    %- authentication_pubkey_challengeresponse = "Sect.  4.2.2" -%
 892    4.2.2  The Challenge-Response Sequence
 893
 894      The authentication sequence begins when the client asserts its
 895      identity via an AUTH command, providing its claimed uid and raw
 896      Ed25519 public key (encoded as an unpadded Base64URL string).  The
 897      server immediately verifies that the uid is a valid deterministic
 898      derivative of the provided public key.
 899
 900      Upon successful validation, the server generates a 32-byte
 901      cryptographically secure pseudorandom nonce.  It transmits this
 902      nonce to the client as a 64-character lowercase hexadecimal string
 903      within a CHALLENGE message.  To mitigate replay attacks and
 904      resource exhaustion, the server MUST restrict this nonce to a
 905      single use and enforce a strict 60-second Time-To-Live (TTL).
 906
 907      To complete the cryptographic proof, the client returns a second
 908      AUTH command containing a signature in the sig tag.  This
 909      follow-up command MUST NOT contain a message body.
 910
 911    %- authentication_pubkey_signatureconstruction = "Sect.  4.2.3" -%
 912    4.2.3  Signature Construction
 913
 914      The client generates the Ed25519 signature over a composite binary
 915      payload, designated as signed_msg. This payload is the strict
 916      concatenation of three elements: the 32-byte binary representation
 917      of the server’s hex-encoded nonce, the 26-byte raw UTF-8 string of
 918      the server’s sid, and an 8-byte big-endian unsigned integer
 919      representing the server’s ts in milliseconds.
 920
 921        signed_msg = nonce_bytes || sid_bytes || ts_bytes
 922
 923      By including the sid and ts (both acquired during the Connection
 924      Prelude), the protocol binds the signature to a specific server
 925      instance and a specific point in time, providing robust protection
 926      against connection hijacking and cross-server replay.
 927
 928      (See %-implementation_notes_2-% for the normative reference
 929      implementation in Go.)
 930
 931    %- authentication_pubkey_verification = "Sect.  4.2.4" -%
 932    4.2.4  Server Verification Procedure
 933
 934      Upon receiving the cryptographic proof, the server MUST execute
 935      the following verification sequence. Any failure during this
 936      process MUST trigger an immediate ERR code=AUTHFAIL and terminate
 937      the transport connection.
 938
 939        1.  Decode the hex nonce and verify it exists in the active
 940            state table, remains unconsumed, and has not exceeded its
 941            60-second TTL.
 942        2.  Reconstruct the signed_msg locally, utilizing the active
 943            session’s sid and the original ts issued during the HELLO
 944            exchange.
 945        3.  Verify the client’s provided sig against the locally
 946            reconstructed signed_msg using the initially asserted public
 947            key.
 948        4.  Re-derive the uid from the public key to guarantee it
 949            perfectly matches the uid claimed in the initial assertion.
 950        5.  Mark the nonce as consumed and transition the session state
 951            to "Authenticated."
 952
 953    %- authentication_operationalconsiderations = "Sect.  4.2.5" -%
 954    4.2.5  Operational Considerations
 955
 956      Because the ts embedded within the signed message is the exact
 957      value yielded during the HELLO phase, the server relies entirely
 958      on its own internal clock for signature reconstruction.  The
 959      client’s local system clock is mathematically irrelevant to the
 960      signature’s validity, rendering client-server clock skew a
 961      non-issue.  The sole temporal constraint is the server-enforced
 962      60-second TTL on the active nonce.
 963
 964      If the server’s authoritative datastore lacks a pre-existing
 965      record for the validated uid, the server seamlessly instantiates a
 966      new identity record, persisting the presented public key.
 967      Subsequent authentications utilizing the same key will succeed
 968      transparently.
 969
 970      If the server possesses an existing record for the uid, but the
 971      stored public key diverges from the key currently being presented,
 972      the server MUST reject the attempt with ERR code=AUTHFAIL and MUST
 973      NOT overwrite the stored key.  Identity recovery and key
 974      replacement dictate the strict use of the KEYROTATE protocol (see
 975      %-authentication_keyrotation-%).
 976
 977  %- authentication_tokenauthentication = "Sect.  4.3" -%
 978  4.3  Token Authentication
 979
 980    Token authentication provides a highly streamlined,
 981    single-round-trip alternative expressly designed for non-interactive
 982    clients, such as automated bots or headless services.
 983
 984    %- authentication_token_overview = "Sect.  4.3.1" -%
 985    4.3.1  Protocol Exchange Overview
 986
 987      CLIENT                              SERVER
 988      ──────                              ──────
 989      AUTH method=TOKEN uid=<uid>
 990          :token                  ────►
 991                                   ◄────  OK uid=<uid> :welcome
 992                                          (or ERR code=AUTHFAIL :reason)
 993
 994    %- authentication_token_issuance = "Sect.  4.3.2" -%
 995    4.3.2  Token Issuance and Scope
 996
 997      A token functions as an opaque bearer credential, provisioned by
 998      the server through out-of-band channels (such as a secured HTTP
 999      API or embedded within a KEYROTATE response payload).  These
1000      tokens are inextricably bound to a specific (uid, server) tuple.
1001      Consequently, a token issued by Server A holds no authority and
1002      remains strictly invalid if presented to Server B.
1003
1004    %- authentication_token_validation = "Sect.  4.3.3" -%
1005    4.3.3  Transport and Validation Constraints
1006
1007      While the internal formatting of the token is an implementation
1008      detail completely opaque to the WIRE protocol, clients MUST
1009      restrict token transmission exclusively to transport layers
1010      secured by TLS.  During the exchange, the uid tag provided in the
1011      AUTH command MUST perfectly align with the internal uid bound to
1012      the token.  Servers MUST reject any mismatched assertions with an
1013      immediate ERR code=AUTHFAIL.
1014
1015    %- authentication_token_operational = "Sect.  4.3.4" -%
1016    4.3.4  Operational Considerations
1017
1018      By eschewing the cryptographic challenge-response sequence, token
1019      authentication achieves high efficiency via a single network
1020      round-trip.  However, this inherently forfeits the strict
1021      cryptographic proof-of-possession that underpins the primary
1022      PUBKEY vector.  Therefore, token authentication is NOT RECOMMENDED
1023      as the primary mechanism for user-facing clients.
1024
1025      If a bearer token reaches its administrative expiry threshold
1026      while maintaining an active session, the server MUST unilaterally
1027      sever the connection and issue ERR code=NOTAUTH :token expired.
1028      The server SHALL NOT provide forewarning, renegotiation, or a
1029      grace period.  The client MUST acquire a fresh token or revert to
1030      PUBKEY authentication before attempting reconnection.
1031
1032  %- authentication_preauthentication_restrictions = "Sect.  4.4" -%
1033  4.4  Pre-Authentication Restrictions
1034
1035    Until the server transmits an OK response confirming a successful
1036    authentication exchange, it MUST maintain a heavily restricted
1037    security posture.  During this transient phase, the server is
1038    permitted to process only a minimal subset of the protocol’s command
1039    vocabulary.
1040
1041    %- authentication_restricted_verbs = "Sect.  4.4.1" -%
1042    4.4.1  Permitted Command Subset
1043
1044      The server SHALL ONLY acknowledge the following verbs in a
1045      pre-authenticated context:
1046
1047      - HELLO: For protocol versioning and session initialization.
1048      - AUTH: To execute the primary or secondary authentication flows.
1049      - CAPS: To negotiate optional protocol capabilities.
1050      - PING: To verify transport layer liveness.
1051      - SRVADM (op=policy): To retrieve the access policies
1052        governing the instance.
1053
1054      Any verb not explicitly listed above MUST be abruptly rejected
1055      with ERR code=NOTAUTH, and the server SHOULD NOT process the
1056      request body.
1057
1058    %- authentication_srvadm_exception = "Sect.  4.4.2" -%
1059    4.4.2  SRVADM Scope and Logic
1060
1061      The SRVADM op=policy command is uniquely permitted prior to
1062      authentication because it provides the client with the necessary
1063      parameters to satisfy the server’s specific access requirements.
1064      However, this permission is strictly limited.  All other
1065      administrative operations, including but not limited to
1066      op=role.assign, op=role.revoke, op=perm.grant, op=perm.revoke,
1067      op=list, and op=info, MUST be rejected if attempted before a
1068      session is fully authenticated.
1069
1070      NOTE:
1071
1072        Requests for prohibited administrative actions in this phase
1073        MUST return ERR code=NOTAUTH rather than ERR code=NOPERM.  The
1074        NOPERM code is reserved exclusively for authenticated users who
1075        possess a valid identity but lack the requisite administrative
1076        authorization to perform the requested operation.
1077
1078  %- authentication_identity = "Sect.  4.5" -%
1079  4.5  Post-Auth: Display Name and Profile (IDENTIFY)
1080
1081    Once a session has transitioned to the “Authenticated” state, a
1082    client MAY declare or update its human-readable profile parameters
1083    using the IDENTIFY verb.  This mechanism allows the underlying
1084    cryptographic uid to be associated with a display name and an
1085    optional avatar.
1086
1087    %- authentication_identify_syntax = "Sect.  4.5.1" -%
1088    4.5.1  Command Syntax and Constraints
1089
1090      The IDENTIFY command utilizes the following structure:
1091
1092        IDENTIFY name=<urlencoded-displayname> [avatar=<aid|https-url>]
1093
1094      Upon receipt, the server updates its internal directory mapping
1095      the uid to the provided display name.
1096
1097      The name tag value MUST be between 1 and 64 decoded characters.
1098      It MUST consist exclusively of printable UTF-8 characters; control
1099      characters are strictly prohibited.
1100
1101      The avatar tag value MAY be an aid referencing a previously
1102      UPLOADED internal asset, or a percent-encoded (as per
1103      %-lineformat_tagvalueencoding-%) HTTPS URL.
1104
1105      Implementations designed to prohibit external image loading MUST
1106      reject HTTP/HTTPS URLs with ERR code=BADARG.
1107
1108    %- authentication_identify_defaulting = "Sect.  4.5.2" -%
1109    4.5.2  Provisioning and Defaulting
1110
1111      The use of IDENTIFY is OPTIONAL.  If a client omits this
1112      configuration, the server SHALL assign a default display name,
1113      typically a truncated hexadecimal representation of the uid.  Upon
1114      a successful update, the server confirms the profile change by
1115      replying to the originating client:
1116
1117        OK verb=IDENTIFY uid=<uid> ts=<unix-ms>
1118
1119      The explicit inclusion of uid= in the OK response is crucial.
1120      Because IDENTIFY is the operation that associates a uid with a
1121      display name for the first time, the client state machine may not
1122      yet have fully committed the derived uid to local storage. Echoing
1123      the uid provides necessary confirmation.
1124
1125    %- authentication_identify_propagation = "Sect.  4.5.3" -%
1126    4.5.3  Profile Updates and Propagation
1127
1128      Established users MAY issue an IDENTIFY command at any time to
1129      refresh their nomenclature or avatar.  In such instances, the
1130      server MUST broadcast the updated profile to all active users who
1131      share a channel with the acting uid via a PROFILE event:
1132
1133        PROFILE uid=<uid> name=<urlencoded-name>
1134                [avatar=<aid|https-url>] ts=<unix-ms>
1135
1136      NOTE:
1137        IDENTIFY is the only mutating verb designed to echo the acting
1138        uid; standard mutating verbs typically echo only the identifiers
1139        of the resources they modified.
1140
1141  %- authentication_keyrotation = "Sect.  4.6" -%
1142  4.6  Key Rotation (KEYROTATE)
1143
1144    The WIRE protocol provides a first-class mechanism for cryptographic
1145    key rotation, enabling users to replace an active keypair in
1146    response to compromise, routine hygiene, or operational policy.  The
1147    KEYROTATE procedure is designed to ensure continuity of identity
1148    while requiring explicit cryptographic authorization from both the
1149    legacy and successor keypairs.
1150
1151    A rotation sequence may only be initiated from a session that has
1152    already completed full authentication using the PUBKEY method with
1153    the currently active (legacy) credentials.  This requirement ensures
1154    that only a legitimately authenticated principal may attempt to
1155    transition the identity.
1156
1157    %- authentication_keyrotate_overview = "Sect.  4.6.1" -%
1158    4.6.1  Protocol Exchange Overview
1159
1160      The KEYROTATE exchange proceeds as a challenge-response sequence
1161      in which the client first proposes a new public key, and then
1162      proves control over both the existing and proposed identities:
1163
1164      CLIENT                              SERVER
1165      ──────                              ──────
1166
1167      KEYROTATE newkey=<b64url-newpubkey> ────►
1168                                   ◄────  CHALLENGE nonce=<hex32>
1169
1170      KEYROTATE sig_old=<b64url-sig-from-old-key>
1171                sig_new=<b64url-sig-from-new-key> ────►
1172                                   ◄────  OK uid=<new-uid>
1173                                          (or ERR code=AUTHFAIL :reason)
1174
1175      The server-issued nonce binds the operation to a fresh,
1176      non-replayable context, while the dual-signature requirement
1177      ensures that both key generations explicitly authorize the
1178      transition.
1179
1180    %- authentication_keyrotate_payload = "Sect.  4.6.2" -%
1181    4.6.2  Rotation Signature Payload
1182
1183      To enforce strict cryptographic domain separation and prevent
1184      signature reuse across protocol contexts (for example, reusing an
1185      AUTH signature for a KEYROTATE operation), the rotation payload is
1186      defined as a distinct structured message:
1187
1188        rotate_msg = nonce_bytes || sid_bytes ||
1189                     old_uid_bytes || new_uid_bytes
1190
1191      Each component is defined as follows:
1192
1193        - nonce_bytes
1194            The 32-byte binary form of the server-provided nonce, which
1195            is originally transmitted as a hex-encoded value.
1196
1197        - sid_bytes
1198            The 26-byte UTF-8 encoding of the server’s sid.  Inclusion
1199            of the sid binds the rotation to a specific server instance
1200            and prevents cross-server replay attacks.
1201
1202        - old_uid_bytes
1203            The 26-byte UTF-8 encoding of the currently authenticated
1204            uid.
1205
1206        - new_uid_bytes
1207            The 26-byte UTF-8 encoding of the uid derived from the
1208            proposed new public key.
1209
1210      The client MUST produce two signatures over the exact same
1211      rotate_msg: sig_old, generated using the legacy private key, and
1212      sig_new, generated using the new private key.  By binding both
1213      identifiers and the server-specific context into a single payload,
1214      the protocol guarantees that the rotation is jointly authorized
1215      and non-transferable.
1216
1217    %- authentication_keyrotate_preconditions = "Sect.  4.6.3" -%
1218    4.6.3  Normative Preconditions
1219
1220      The server MUST verify that the legacy key corresponds to the
1221      canonical, currently active credential for the authenticated uid.
1222      Any KEYROTATE attempt referencing a uid that has already been
1223      rotated (and therefore exists only as an alias) MUST be rejected
1224      with ERR code=AUTHFAIL.
1225
1226      Additionally, the uid derived from the proposed new key MUST be
1227      globally unique within the server’s datastore.  It MUST NOT
1228      collide with any active uid or any historical alias.  Violations
1229      of this constraint MUST result in rejection with ERR
1230      code=AUTHFAIL.
1231
1232    %- authentication_keyrotate_succees = "Sect.  4.6.4" -%
1233    4.6.4  Normative Success Behavior
1234
1235      Upon successful validation of both signatures, the server MUST
1236      execute the rotation as a single, strictly atomic transaction. The
1237      following effects are required:
1238
1239      First, all other active sessions authenticated under the legacy
1240      uid MUST be immediately disconnected.  This forces all clients to
1241      re-establish authentication under the updated identity state.
1242
1243      Second, the authoritative datastore MUST be updated such that the
1244      newly provided public key becomes the primary credential for the
1245      identity stream.
1246
1247      Third, the legacy uid MUST be permanently retired and recorded as
1248      an immutable alias that resolves directly to the new uid.  Once in
1249      this state, the old uid MUST NOT be accepted as a source identity
1250      for any future KEYROTATE operations.
1251
1252      Fourth, all relational state—including, but not limited to,
1253      community memberships, role assignments, and message history—MUST
1254      remain logically continuous.  The new uid becomes the canonical
1255      identifier without requiring physical data migration.
1256
1257      Fifth, the server MUST broadcast a migration event to all peers
1258      that share communities with the user:
1259
1260        PROFILE uid=<new-uid> name=<n> prev_uid=<old-uid> ts=<unix-ms>
1261
1262      The presence of the prev_uid field instructs clients to merge any
1263      cached state associated with the legacy identity into the new one,
1264      preserving continuity at the application layer.
1265
1266      Finally, the initiating session MUST receive:
1267
1268        OK uid=<new-uid>
1269
1270      From that point forward, the connection is considered fully
1271      authenticated under the new uid, and all subsequent protocol
1272      operations are evaluated within that updated identity context.
1273
1274      If any step within this sequence fails, the server MUST perform a
1275      complete rollback to the pre-rotation state and return ERR
1276      code=AUTHFAIL.  Partial commits are explicitly forbidden.
1277
1278    %- authentication_keyrotate_graceperiod = "Sect.  4.6.5" -%
1279    4.6.5  Grace Period Implementation
1280
1281      To accommodate users operating multiple or intermittently
1282      connected devices, servers SHOULD implement a bounded grace period
1283      following a successful rotation.  A duration of 30 days is
1284      RECOMMENDED.
1285
1286      During this interval, standard AUTH requests signed with the
1287      legacy key remain valid.  However, successful authentication using
1288      such credentials MUST return:
1289
1290        OK uid=<new-uid> key_deprecated=1
1291
1292      The key_deprecated=1 marker serves as an explicit signal to the
1293      client that the credential is no longer authoritative and SHOULD
1294      be replaced.  Client implementations are expected to prompt the
1295      user to generate a new keypair or synchronize credentials from a
1296      primary device.
1297
1298      Internally, the server MUST resolve all legacy-key authentications
1299      directly to the new uid.  All actions performed under such
1300      sessions are canonically attributed to the new identity.
1301
1302      To prevent replay or duplication of rotation attempts during the
1303      grace window, servers MUST maintain a dedicated rotation ledger
1304      keyed by (old_uid, new_uid).  Any KEYROTATE request that attempts
1305      to reproduce an already recorded transition MUST be rejected with
1306      ERR code=AUTHFAIL, bypassing standard nonce validation entirely.
1307
1308  %- authentication_replayprotection = "Sect.  4.7" -%
1309  4.7  Replay Protection
1310
1311    The integrity of both the PUBKEY and KEYROTATE authentication flows
1312    depends critically on the strict single-use property of the
1313    CHALLENGE nonce.  Each nonce represents a unique, time-bound
1314    authorization context and MUST NOT be reused under any
1315    circumstances.
1316
1317    To enforce this guarantee, servers MUST maintain an active set of
1318    previously used nonces.  Each entry in this set is subject to a
1319    strict Time-To-Live (TTL) of 60 seconds.  Any authentication payload
1320    that attempts to reuse a nonce still residing within this validity
1321    window MUST be rejected immediately with ERR code=AUTHFAIL.
1322
1323    Upon expiration of the 60-second TTL, a nonce may be safely removed
1324    from the active set.  At that point, replay attempts are
1325    independently mitigated by the temporal validation inherent in the
1326    protocol, as the nonce will no longer satisfy freshness
1327    requirements.  As a result, the total memory footprint of the
1328    used-nonce set is deterministically bounded and can be expressed as:
1329
1330      maximum entries ≈ maximum authentications per second × 60
1331
1332    Implementations MAY impose a strict upper bound on the size of the
1333    nonce set as a defensive measure.  In scenarios involving extreme
1334    authentication throughput or adversarial flooding, where this
1335    capacity is exhausted, the server is permitted to temporarily reject
1336    or drop incoming authentication requests.  This behavior is
1337    considered compliant, provided it is necessary to preserve overall
1338    system stability.
1339
1340%- namespaces_resources = "Sect.  5" -%
1341========================================================================
1342%-namespaces_resources-% -- NAMESPACES AND RESOURCES
1343========================================================================
1344
1345  %- namespaces_resources_overview = "Sect.  5.1" -%
1346  5.1  Overview
1347
1348    The WIRE protocol organizes all protocol-visible state around three
1349    primary resource types: the server, the community, and the channel.
1350    A server represents a single WIRE instance and constitutes the root
1351    of the namespace.  Within a server, a community provides a named
1352    grouping that encapsulates roles, branding, and an associated
1353    collection of channels.  A channel, in turn, represents a message
1354    stream that may either belong to a community or, in the case of
1355    direct messaging, exist directly under the server.
1356
1357    These resources are addressed through resource identifiers (rid),
1358    which together form a hierarchical namespace spanning all protocol
1359    primitives.
1360
1361  %- namespaces_resources_rid = "Sect.  5.2" -%
1362  5.2  Resource Identifiers
1363
1364    A resource identifier (rid) is a structured path that uniquely
1365    identifies a resource within the namespace of a server.  The
1366    protocol defines several canonical forms:
1367
1368      @uid
1369          Identifies a direct message (DM) channel with the specified
1370          user.
1371
1372      #cid
1373          Refers to a channel by its opaque ULID.  This form is valid
1374          only in markup contexts (see %-wire_markup_2-%) and MUST NOT
1375          be used as a rid in protocol operations.
1376
1377      /hostname
1378          Identifies the server root.  The hostname is obtained from the
1379          HELLO payload as defined in
1380          %-authentication_connectionprelude-%.
1381
1382      /hostname/slug
1383          Identifies a community.  The slug is derived as specified in
1384          %-identifiers-%.
1385
1386      /hostname/slug/chanslug
1387          Identifies a channel within a community.  The channel slug is
1388          assigned at creation time (COMM op=channel.create) and is
1389          returned in CHAN op=info via the slug= field.
1390
1391    Resource identifiers are constructed exclusively from slugs. Display
1392    names are never embedded in the path.  For example, a channel named
1393    "General Chat" with slug "general-chat" is addressed as:
1394
1395      /wire.example.org/acme/general-chat
1396
1397    The display name is transmitted independently (for example,
1398    name=General%20Chat in CHAN op=info).  Slugs function as stable,
1399    machine-oriented identifiers, whereas display names are
1400    human-readable and may change without affecting addressing.
1401
1402    Rids are transmitted as tag values and are therefore subject to the
1403    encoding rules defined in %-lineformat_tagvalueencoding-%.  Because
1404    slugs are restricted to the character set [a-z0-9_-], they do not
1405    require percent-encoding.  A complete rid is emitted verbatim as a
1406    tag value without additional transformation.
1407
1408    Examples:
1409
1410      /wire.example.org/acme/general
1411      /wire.example.org/acme/announcements
1412      @7ZNKQ5XMVP2R4ABCDE01JXAAA
1413
1414    The @uid form is a local shorthand that omits the hostname and is
1415    resolved relative to the current server context.
1416
1417  %- namespaces_resources_hostnamehandling = "Sect.  5.3" -%
1418  5.3  Hostname Handling
1419
1420    The hostname component of a rid is derived from the HELLO payload,
1421    as specified in %-authentication_connectionprelude-%.  Servers MUST
1422    accept fully qualified rids that begin with their configured
1423    hostname.
1424
1425    For convenience, servers MAY also accept hostname-omitted forms,
1426    such as "/acme/general", resolving them implicitly against their own
1427    hostname.  This behavior is OPTIONAL.  Clients SHOULD prefer fully
1428    qualified rids to ensure portability and to avoid ambiguity,
1429    particularly in federated deployments (see
1430    %-server_to_server_federation-%).
1431
1432  %- namespaces_resources_subscribing = "Sect.  5.4" -%
1433  5.4  Subscribing to Resources
1434
1435    Clients may subscribe to channels using either a human-readable rid
1436    or an opaque channel identifier (cid).  These forms are strictly
1437    equivalent:
1438
1439      SUB rid=<rid>
1440      SUB cid=<cid>
1441
1442    In practice, the rid form is typically used during discovery, while
1443    the cid form is preferred once the mapping has been resolved and
1444    persisted locally.
1445
1446    Servers MUST accept both forms unconditionally.  A SUB request that
1447    omits both rid= and cid= MUST be rejected with ERR code=BADARG.
1448
1449    Regardless of the request form, the server response is normalized. A
1450    successful subscription MUST include both rid= and cid=:
1451
1452      OK verb=SUB rid=<rid> cid=<cid>
1453
1454    When the client supplies cid=, the server derives the corresponding
1455    rid from its internal channel record.  For community channels, this
1456    is the fully qualified path.  For DM channels, the value is the
1457    stored per-(uid, cid) mapping (see
1458    %-namespaces_resources_dmchannels-%). This symmetry eliminates any
1459    dependency on the original request form.
1460
1461    After subscription, all subsequent events for the channel are
1462    identified exclusively by cid=<cid>.  The rid is not repeated in
1463    event lines.  Clients are responsible for maintaining the rid-to-cid
1464    mapping.
1465
1466  %- namespaces_resources_communitymembershipreqs = "Sect.  5.5" -%
1467  5.5  Community Membership Requirements
1468
1469    Subscription to a community channel is contingent upon prior
1470    membership in that community.  A client MUST successfully join the
1471    community (COMM op=join) before issuing SUB for any channel within
1472    it.  Attempts to subscribe without membership MUST be rejected with
1473    ERR code=NOPERM.
1474
1475    Direct message channels are explicitly exempt from this requirement.
1476
1477  %- namespaces_resources_dmchannels = "Sect.  5.6" -%
1478  5.6  Direct Message Channels
1479
1480    A direct message (DM) channel is a private, bidirectional message
1481    stream between exactly two users.  Such channels do not belong to
1482    any community and MUST NOT appear in COMM op=list or COMM
1483    op=channel.list.
1484
1485    DM channels are created implicitly upon first subscription:
1486
1487      SUB rid=@<target-uid>
1488
1489    If no DM channel exists between the authenticated user and the
1490    target, the server MUST create one atomically and return its cid:
1491
1492      OK verb=SUB rid=@<target-uid> cid=<cid>
1493
1494    The cid of a DM channel is both stable and symmetric.  Both
1495    participants MUST observe the same cid regardless of which party
1496    initiates the subscription.  Concretely:
1497
1498      SUB rid=@<target-uid> issued by uid A
1499      SUB rid=@<uid-A> issued by target-uid
1500
1501    MUST resolve to the same cid.
1502
1503    Once created, DM channels are permanent and MUST NOT be deleted by
1504    the server.  Users MAY SUB or UNSUB such channels at any time;
1505    however, unsubscribing does not destroy the channel or its history.
1506
1507    Each DM subscription is associated with a rid stored per (uid, cid)
1508    pair.  Accordingly, the same cid appears as @<uid-B> for uid-A and
1509    as @<uid-A> for uid-B.
1510
1511    The target uid MUST correspond to a known user on the server, that
1512    is, one that has previously authenticated.  Otherwise, the request
1513    MUST be rejected with ERR code=NOTFOUND.
1514
1515    DM channels generate PING kind=user events for the recipient on
1516    every message (see %-messages_ping-%).  Role mentions (rolmentions=)
1517    are not valid within DM channels and MUST be rejected with ERR
1518    code=BADARG.
1519
1520  %- namespaces_resources_dmcreation = "Sect.  5.7" -%
1521  5.7  DM Creation Policies
1522
1523    Certain properties of a DM channel are defined exclusively at
1524    creation time, that is, during SUB rid=@<uid> when no channel yet
1525    exists.  These properties are immutable thereafter.  Supplying such
1526    fields when targeting an existing DM or any non-DM channel MUST
1527    result in ERR code=BADARG.
1528
1529    Servers MUST persist these fields as part of the DM channel record
1530    and echo them in the SUB response.
1531
1532    The delete_policy field governs message deletion semantics and
1533    accepts the following values:
1534
1535      unilateral
1536          Either participant may independently purge the DM history.
1537          This is the default when the field is omitted.
1538
1539      mutual
1540          HIST op=purge requires explicit confirmation from both
1541          participants.
1542
1543    Immutability of this field prevents retroactive weakening of
1544    deletion constraints.
1545
1546    The e2e field enables end-to-end encryption:
1547
1548      e2e=0|1 (default: 0)
1549
1550    When enabled, the channel operates using Layer 1 encryption (X3DH
1551    combined with the Double Ratchet), as defined in
1552    %-history_purge_e2ee_2-%.  The enc=skdm mechanism is not used for DM
1553    channels.
1554
1555    In SUB responses, delete_policy MUST always be present.  The e2e
1556    field MUST be omitted when set to its default value (0), in
1557    accordance with the tag-absence rule (see
1558    %-lineformat_tagsemantics-%).
1559
1560    These fields MUST NOT be accepted for community channels.
1561
1562  %- namespaces_resources_subdurability = "Sect.  5.8" -%
1563  5.8  Subscription Durability
1564
1565    Subscriptions are durable and scoped to the user identity (uid),
1566    rather than to any specific connection or session.  A subscription
1567    represents persistent intent to receive events from a channel across
1568    all devices and sessions associated with that uid.
1569
1570    Servers MUST persist the subscription set per uid and restore it
1571    upon re-authentication.  Following AUTH, the server MUST reactivate
1572    all stored subscriptions on the newly established connection as if
1573    the client had reissued SUB for each channel.  This restoration
1574    occurs after the PING burst (see %-messages_ping-%).
1575
1576    The SUB command adds a channel to the durable set and activates
1577    delivery on the current connection.  Conversely, UNSUB removes the
1578    channel from the durable set.  UNSUB requests that omit cid= MUST be
1579    rejected with ERR code=BADARG.
1580
1581  %- namespaces_resources_sublisting = "Sect.  5.9" -%
1582  5.9  Subscription Listing
1583
1584    Clients may enumerate subscriptions using:
1585
1586      SUB op=list [offset=<n>] [limit=<n>]
1587
1588    The default offset is 0, the default limit is 50, and the maximum
1589    permitted limit is 200.  Servers MUST clamp limit to 200.
1590
1591    The server responds with one SUB op=info line per subscription,
1592    followed by:
1593
1594      OK verb=SUB op=list count=<n> total=<n>
1595
1596    Each SUB op=info line includes both cid=<cid> and rid=<rid>.  The
1597    rid is the fully qualified value associated with the uid, while cid
1598    is the opaque ULID.
1599
1600    The total field represents the complete size of the subscription
1601    set.  Because this set is owned by the uid and mutates only through
1602    explicit client action, it remains stable throughout the query. This
1603    behavior differs from unbounded streams such as HIST or LIST, which
1604    instead rely on has_more= (see %-lineformat_listshaperules-%).
1605
1606  %- namespaces_resources_subgc = "Sect.  5.10" -%
1607  5.10  Subscription Garbage Collection
1608
1609    Servers MUST automatically remove subscriptions under specific
1610    conditions to preserve consistency:
1611
1612      - When a channel is deleted (CHAN op=delete), the server removes
1613        the cid from all subscription sets prior to closing active
1614        connections.
1615
1616      - When a user leaves, is removed from, or is banned from a
1617        community (COMM op=leave, MEMBER op=kick, MEMBER op=ban), all
1618        subscriptions to channels within that community are removed.
1619
1620      - When a community is deleted (COMM op=delete), all subscriptions
1621        to its channels are removed for all users.
1622
1623    A user who is banned and subsequently unbanned does not regain any
1624    prior subscriptions and MUST explicitly resubscribe after rejoining.
1625
1626  %- namespaces_resources_multiconnsemantics = "Sect.  5.11" -%
1627  5.11  Multi-Connection Semantics
1628
1629    When a user maintains multiple concurrent sessions, all sessions
1630    share a single, unified subscription set.  A SUB or UNSUB issued on
1631    any session applies immediately and globally to all sessions
1632    associated with that uid.
1633
1634    Servers MUST deliver events to all active connections of a
1635    subscribed user, not solely to the connection that initiated the
1636    subscription.
1637
1638    This behavior is consistent with other uid-scoped state, including
1639    read markers and notification dismissal, which are likewise shared
1640    across sessions.
1641
1642%- messages = "Sect.  6" -%
1643========================================================================
1644%-messages-% -- MESSAGES
1645========================================================================
1646
1647  %- messages_sending = "Sect.  6.1" -%
1648  6.1  Sending
1649
1650    The MSG verb is the primary mechanism for publishing content to a
1651    channel.  A client MUST supply cid=<cid> on all client-originated
1652    MSG lines, as the server requires the channel identifier at
1653    ingestion time to route the message prior to assigning a mid.  This
1654    differs from REACT and UNREACT (see %-reactions-%), which omit cid=
1655    in their client forms because the server can resolve the channel
1656    from its mid→cid index.
1657
1658    The complete syntax is:
1659
1660      MSG cid=<cid> [tid=<tid>] [ref=<mid>]
1661          [mentions=<csv-uid>]
1662          [rolmentions=<csv-rolid>]
1663          [attachments=<csv-aid:urlencoded-mime>]
1664          [title=<urlencoded-title>]
1665          [tags=<csv>]
1666          [enc=xchacha20|skdm] [target=<uid>]
1667          :body
1668
1669
1670    The tid= field identifies a thread.  Its semantics vary by channel
1671    type.  On text channels, tid= associates the message with an
1672    existing thread; absence of tid= produces a top-level message. On
1673    forum channels, absence of tid= combined with presence of title=
1674    creates a new thread, with the server assigning tid= in the
1675    broadcast.  If tid= is present on a forum channel, the message is a
1676    reply and title= MUST NOT be present.  On board channels, tid= is
1677    not used; any MSG carrying tid= MUST be rejected with ERR
1678    code=BADARG.  On voice channels, tid= is ignored.
1679
1680    The ref= field carries the mid of a quoted or replied-to message. It
1681    has no server-side semantics and is used solely for client-side
1682    rendering.  It is valid on all channel types.
1683
1684    The mentions= field is a comma-separated list of uid values that are
1685    explicitly referenced.  Servers MUST validate that each uid is a
1686    member of the relevant community.
1687
1688    The rolmentions= field contains role identifiers.  This includes
1689    built-in roles such as 'everyone' and 'here' (see
1690    %-community_management_2-%).  Servers MUST validate that each rolid
1691    exists or is a valid built-in, and MUST enforce mention permissions.
1692
1693    The attachments= field declares a comma-separated list of aid%3Amime
1694    pairs corresponding to attachments referenced in the body.
1695    Examples:
1696
1697      aid        MIME type          aid:mime (percent-encoded)
1698      ───────    ───────────────    ───────────────────────────
1699      01JXAAA    image/png          01JXAAA%3Aimage%2Fpng
1700      01JXBBB    application/pdf    01JXBBB%3Aapplication%2Fpdf
1701
1702    The client MUST include one entry per aid:<aid> reference. The mime
1703    value MUST match that provided at UPLOAD time.  Servers MUST NOT
1704    parse the body to derive this field. Instead, they validate that
1705    each aid corresponds to a previously uploaded and confirmed asset.
1706    Invalid entries are stripped and reported via WARN
1707    code=ATTACH_STRIPPED.  The server relays surviving entries verbatim
1708    without normalization or re-derivation.  The tag MUST be omitted
1709    entirely when no attachments are present.  A tag with an empty value
1710    violates %-lineformat_tagsemantics-%.
1711
1712    The title= field is percent-encoded and limited to 256 decoded
1713    characters.  It is required for the first message in a forum thread
1714    and for all board posts.  It MUST NOT be present on thread replies.
1715    Servers MUST ignore title= on text channels.
1716
1717    The tags= field provides up to 20 labels, each up to 64 characters.
1718    It is meaningful only for board channels and MUST be ignored
1719    elsewhere.
1720
1721    The body is encoded per %-lineformat_payloadencoding-%.  Newlines
1722    use the \n escape.  The body MAY be empty on board posts.
1723
1724    After validation, the server responds:
1725
1726      OK verb=MSG cid=<cid> mid=<mid> [tid=<tid>] ts=<unix-ms>
1727
1728    The mid is a server-assigned ULID.  On forum channels where a new
1729    thread is created, tid= is included so the client learns the thread
1730    identifier immediately.  The OK is sent before the broadcast.
1731
1732    The mentions=, rolmentions=, and attachments= fields constitute
1733    structural metadata and are explicitly distinct from any markup
1734    contained within the message body (see %-wire_markup-%).  Clients
1735    MUST maintain consistency between these representations, however
1736    servers do not enforce or validate this relationship.
1737
1738    These fields are not markup.  The body MAY independently contain
1739    @uid:name, @rolid:name, and ![alt](aid:<aid>) constructs for
1740    client-side rendering (see %-wire_markup-%).  The structural tags
1741    and the body markup are orthogonal and serve distinct purposes.
1742
1743    The mentions= and rolmentions= fields drive server-side PING
1744    delivery (see %-messages_ping-%).  The attachments= field enables
1745    receivers to render attachments without requiring a subsequent
1746    ATTACH resolution round-trip.  In contrast, markup within the body
1747    exists solely for client-side presentation, including highlighting,
1748    styling, and inline media rendering.
1749
1750    Clients MUST keep structural tags and body markup consistent.
1751    Servers do NOT parse the body to derive or validate this
1752    consistency.  A message that includes attachments= but no
1753    corresponding aid: markup in the body is valid.  Conversely, body
1754    markup referencing aid:<aid> without a corresponding attachments=
1755    entry remains valid, but receiving clients MUST fall back to ATTACH
1756    resolution to obtain the referenced asset metadata.
1757
1758  %- messages_receiving = "Sect.  6.2" -%
1759  6.2  Receiving
1760
1761    After issuing the OK response, the server broadcasts the message to
1762    all subscribers of the channel, including the sender:
1763
1764      MSG cid=<cid> mid=<mid> uid=<uid> ts=<unix-ms>
1765          [tid=<tid>] [ref=<mid>]
1766          [mentions=<csv-uid>]
1767          [rolmentions=<csv-rolid>]
1768          [attachments=<csv-aid:urlencoded-mime>]
1769          [title=<urlencoded-title>]
1770          [tags=<csv>]
1771          [enc=xchacha20|skdm] [target=<uid>]
1772          :body
1773
1774    The ts field is expressed as a Unix epoch timestamp in milliseconds.
1775    On forum channels, if a thread is created implicitly, the assigned
1776    tid= is included in both the OK and the broadcast.
1777
1778    Servers relay mentions= and rolmentions= after validation. Invalid
1779    uid entries are dropped and reported via WARN
1780    code=MENTION_UID_INVALID (see %-adversarial_behavior_2-%). Role
1781    mention validation follows a three-tier rule: insufficient
1782    permission for 'everyone' or 'here' causes a hard reject; lack of
1783    permission for other roles results in the entire tag being dropped
1784    with a warning; unknown roles are removed individually.
1785
1786  %- messages_ordering = "Sect.  6.3" -%
1787  6.3  Per-CID Ordering Guarantee
1788
1789    Servers MUST deliver MSG events for a given cid in a consistent
1790    total order.  The canonical ordering is defined by mid lexicographic
1791    order.  Because mids are ULIDs assigned by the server under a
1792    monotonic rule (see %-identifiers-%), this ordering corresponds to
1793    server receipt order.
1794
1795    No ordering guarantees are made across channels.  Ordering across
1796    servers in federated environments is explicitly unspecified (see
1797    %-server_to_server_federation-%).
1798
1799  %- messages_editing = "Sect.  6.4" -%
1800  6.4  Editing
1801
1802    The EDIT verb modifies an existing message:
1803
1804      EDIT mid=<mid> [tags=<csv>] [:new body]
1805
1806    The server responds:
1807
1808      OK verb=EDIT mid=<mid> cid=<cid> rev=<n> ts=<unix-ms>
1809
1810    and broadcasts:
1811
1812      EDIT mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms> rev=<n>
1813           [enc=xchacha20] [tags=<csv>] :new body
1814
1815    The rev counter starts at 1 for the first edit and increments for
1816    each subsequent edit.  The original message is implicitly revision
1817    0.  The OK is sent before the broadcast, and the rev= value in the
1818    OK MUST match the rev= in the broadcast.  This allows the client to
1819    learn the new revision immediately without waiting for the echo. The
1820    cid= field is included in the OK so the originating client can route
1821    the confirmation without maintaining pending-request state when
1822    pipelining EDIT operations across channels.
1823
1824    Authorization is enforced as follows.  The original author MAY
1825    always edit their own messages.  Any member possessing the
1826    msg.delete permission MAY edit messages authored by others. Servers
1827    MUST reject unauthorized EDIT attempts with ERR code=NOPERM. Servers
1828    MUST store the current body; retention of edit history is optional.
1829
1830    Editing semantics vary by channel type.  On type=board channels, the
1831    tags= field carries the updated tag set for the post.  Body and
1832    tags= are independently optional in this context.  An EDIT that
1833    includes only tags= updates the tag set, an EDIT that includes only
1834    a body payload updates the body, and an EDIT that includes both
1835    updates both atomically.  An EDIT that includes neither body nor
1836    tags= on a board channel MUST be rejected with ERR code=BADARG.
1837
1838    On all non-board channel types, servers MUST silently ignore tags=
1839    in EDIT payloads.  Servers MUST NOT return ERR code=BADARG solely
1840    because tags= was present.  This behavior is symmetric with the MSG
1841    tags= ignore rule (see %-messages_sending-%).  On these channel
1842    types, the body payload is required.
1843
1844    The title= field is immutable after message creation.  Servers MUST
1845    ignore any title= field supplied in an EDIT payload, regardless of
1846    channel type.
1847
1848    The tags= field is included in server-broadcast EDIT lines only when
1849    the tag set has changed on a board channel.  It is absent for
1850    body-only edits and for all non-board channel types.  Clients MUST
1851    update cached tags when tags= is present and MUST NOT clear the
1852    cached tag set when tags= is absent.
1853
1854    EDIT verb directionality is normative.  Parsers MUST use the
1855    presence of uid= to distinguish direction.  A client-sent EDIT line
1856    carries mid=, optional tags= (board channels only), and an optional
1857    body payload.  A server-broadcast EDIT line carries cid=, uid=, ts=,
1858    and rev= in addition to mid=.  Servers MUST NOT emit EDIT lines
1859    without uid=.  Clients MUST NOT send EDIT lines containing uid= or
1860    cid=.  This rule is consistent with DEL (see %-messages_deletion-%),
1861    KVAL (see %-users_presence_5-%), and ATTACH (see %-attachments_3-%).
1862
1863    Inclusion of cid= in the broadcast form ensures that every EDIT line
1864    is self-identifying by channel, consistent with REACT, UNREACT, PIN,
1865    and UNPIN (see %-lineformat_pipelining-%).
1866
1867    Structural tag immutability is strictly enforced.  Structural tags
1868    are the per-message metadata defined at creation time: ref=, tid=,
1869    mentions=, rolmentions=, and attachments=.  These fields are fixed
1870    when the server returns OK verb=MSG and MUST NOT be modified by any
1871    subsequent operation.  EDIT replaces only the message body and, on
1872    board channels, the tags= field.  Servers MUST ignore any attempt to
1873    include structural tags in an EDIT payload, and clients MUST NOT
1874    include them in EDIT requests.
1875
1876  %- messages_deletion = "Sect.  6.5" -%
1877  6.5  Deletion
1878
1879    The DEL verb removes a message identified by its mid:
1880
1881      DEL mid=<mid>
1882
1883    Upon receiving a deletion request, the server responds to the
1884    originating client with an acknowledgment containing the channel
1885    identifier and timestamp:
1886
1887      OK verb=DEL mid=<mid> cid=<cid> ts=<unix-ms>
1888
1889    The server then broadcasts the deletion event to all subscribers:
1890
1891      DEL mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
1892
1893    Directionality is defined normatively.  Parsers MUST use the
1894    presence of uid= to distinguish the direction of a DEL line.  A
1895    client-sent DEL includes only mid=, whereas the server broadcast
1896    includes cid=, uid=, and ts= alongside mid=.  Both cid= and uid= are
1897    broadcast-only fields; clients MUST NOT include them when sending
1898    DEL.  This rule is consistent with the EDIT verb
1899    (%-messages_editing-%).  Including cid= in the broadcast ensures
1900    each DEL line is self-identifying by channel, maintaining
1901    consistency with EDIT, REACT, UNREACT, PIN, and UNPIN
1902    (%-lineformat_pipelining-%).
1903
1904    The cid= tag is echoed in the OK response to allow the originating
1905    client to route the confirmation without maintaining pending-request
1906    state, even when pipelining multiple DELs across channels.  The
1907    server derives cid= from its internal mid→cid index at validation
1908    time; no additional lookup is required.  As with other verbs, the OK
1909    is sent before the broadcast.
1910
1911    Servers MUST NOT return deleted messages in HIST responses, and they
1912    SHOULD tombstone the mid to prevent future ID reuse.
1913
1914  %- messages_history = "Sect.  6.6" -%
1915  6.6  History
1916
1917    Clients retrieve message history using the HIST verb:
1918
1919      HIST cid=<cid> [before=<mid>] [after=<mid>] [limit=<n>]
1920           [tid=<tid>]
1921
1922    In the absence of an explicit limit, the server returns up to 50
1923    events, ordered by ascending mid.  Any client-specified limit is
1924    clamped to a maximum of 200.  The before= and after= parameters are
1925    mutually exclusive; a request that includes both MUST be rejected
1926    with ERR code=BADARG.
1927
1928    When neither before= nor after= is supplied, the server returns the
1929    most recent limit= events in ascending mid order.  This behavior is
1930    intended for initial channel loading.  Clients MAY use HIST in the
1931    following scenarios:
1932
1933      -  Initial load: HIST cid=<cid> limit=50
1934      -  Scroll-up pagination: HIST cid=<cid> before=<oldest-loaded-mid>
1935                               limit=50
1936      -  Catch-up after reconnect: HIST cid=<cid> after=<cursor>
1937                                   limit=50
1938
1939    The optional tid= parameter constrains results to a specific thread.
1940    Its semantics depend on channel type.  On text channels, tid=
1941    identifies the thread and includes the root message.  On forum
1942    channels, it returns all replies belonging to the thread.  On board
1943    channels, where threading is unsupported, the presence of tid= MUST
1944    result in ERR code=BADARG.  The tid= filter MAY be combined with
1945    before= or after= to enable pagination within a thread.
1946
1947    MSG lines returned in HIST MAY contain tags that were not present in
1948    the original live broadcast. Notably, the root message of a
1949    text-channel thread retroactively acquires tid=
1950    (%-boards_forums_threads_3-%). Clients MUST therefore accept and
1951    correctly process MSG lines carrying previously unseen tags.
1952
1953    The server replies with a sequence of event lines sorted by mid,
1954    followed by a terminal OK.  The event stream MAY include:
1955
1956      MSG             -- messages not deleted (current body only)
1957      EDIT            -- most recent revision for each edited message
1958      DEL             -- tombstone for each deleted message
1959      REACT / UNREACT -- all reaction events in the window
1960      PIN / UNPIN     -- pin-related events
1961      THREAD          -- thread rename, lock, or unlock events
1962
1963    THREAD events MUST match the live broadcast format exactly
1964    (%-boards_forums_threads_2a-%).  In this context, THREAD op=rename
1965    includes uid=, ts=, and the updated title, while THREAD op=lock and
1966    op=unlock include uid= and ts= only.  The op=info variant is
1967    strictly a query-reply form and MUST NOT appear in HIST.  Receipt of
1968    such a line outside a pending query MAY be treated by the client as
1969    a protocol error, and the connection MAY be closed.
1970
1971    Event inclusion follows strict rules.  When a message has been
1972    deleted, only the corresponding DEL event is emitted and any MSG is
1973    suppressed.  EDIT events carry the current message body rather than
1974    the original.  Accordingly, HIST does not constitute a faithful
1975    historical replay of edits.  Instead, EDIT lines reflect the body as
1976    it exists at query time, even when the edit occurred after the
1977    requested window boundary.  This design is intentional.  The
1978    protocol does not require servers to retain full revision history;
1979    such functionality is delegated to the HISTREV extension
1980    (%-future_optional_extensions-%).  Clients MUST NOT interpret HIST
1981    as reconstructing message state at an arbitrary past time.  Rather,
1982    HIST reconstructs the current editorial state within a historical
1983    event window.
1984
1985    Each EDIT line in HIST MUST include rev=<n>, representing the
1986    current revision number, and MUST also include cid=, consistent with
1987    the live broadcast form.  Reaction events (REACT and UNREACT) are
1988    included so that clients can reconstruct complete reaction state
1989    through ordered replay.  These lines also carry cid=, as do PIN and
1990    UNPIN events.  A REACT followed by UNREACT from the same (uid, eid)
1991    pair acting on the same mid cancels out logically, and clients are
1992    expected to track only the net effect.  All HIST event lines include
1993    cid=, making them self-identifying and allowing clients to route
1994    events correctly even when multiple HIST requests are pipelined.
1995
1996    The response terminates with:
1997
1998      OK verb=HIST cid=<cid> count=<n> has_more=0|1
1999
2000    The count field denotes the total number of event lines returned,
2001    including MSG, EDIT, DEL, REACT, UNREACT, PIN, UNPIN, and THREAD.
2002    enc=skdm lines (%-history_purge_e2ee_2b-%) are excluded from HIST
2003    responses entirely and do not contribute to count=.  The has_more
2004    indicator signals whether additional events exist beyond the
2005    returned window in the requested direction.  Clients SHOULD rely on
2006    this indicator for pagination rather than inferring completeness
2007    from count, as event density may vary and some windows may be
2008    reaction-dense.  A value of has_more=0 indicates that the boundary
2009    of available history has been reached.  A value of has_more=1
2010    indicates that additional events exist beyond the returned window.
2011    For default (no anchor) and before= responses, has_more=1 indicates
2012    that older history exists, while has_more=0 indicates that the
2013    window includes the start of channel history.
2014
2015  %- messages_bodyformat = "Sect.  6.7" -%
2016  6.7 Body Format
2017
2018    The message body is encoded in accordance with
2019    %-lineformat_payloadencoding-%.  Its content follows the WIRE markup
2020    rules defined in %-wire_markup-%.  Servers are required to store and
2021    relay message bodies verbatim, without transformation or
2022    normalization.
2023
2024    Clients are responsible for decoding escape sequences prior to
2025    applying any markup rendering.  Rendering behavior itself is not
2026    strictly prescribed; clients select an appropriate rendering depth
2027    based on their conformance level and capabilities.
2028
2029  %- messages_ping = "Sect.  6.8" -%
2030  6.8 Mention Delivery (PING)
2031
2032    Following validation of mentions= and rolmentions= tags
2033    (%-messages_sending-%), the server generates PING events for each
2034    resolved target.  These events are delivered out of band, meaning
2035    they are sent directly to the target user’s active connection or
2036    connections, independent of channel subscription state.  Delivery of
2037    PING events does not require channel membership validation at
2038    delivery time; it is strictly driven by mention targeting.
2039
2040    A PING event delivered to a user has the following form:
2041
2042      PING mid=<mid> cid=<cid> [gid=<gid>] uid=<from-uid>
2043           ts=<unix-ms> kind=<kind> [rolid=<rolid>] [count=<n>]
2044
2045    The gid= field is present for community channel PINGs and MUST be
2046    omitted for direct-message channels, which do not belong to a
2047    community (%-namespaces_resources-%).  Clients MUST correctly handle
2048    the absence of gid= and route events using cid= alone in such cases.
2049
2050    The kind field distinguishes between direct user mentions and
2051    role-based mentions.  When kind=user, the receiving uid was
2052    explicitly listed in mentions=.  When kind=role, one of the
2053    recipient’s roles was listed in rolmentions=, and rolid= identifies
2054    the triggering role.  This includes the reserved roles 'everyone'
2055    and 'here', which are not separate kinds but are represented as
2056    kind=role with the corresponding reserved rolid.  The server
2057    resolves these roles to all eligible members and emits one PING per
2058    member, consistent with user-defined roles.
2059
2060    The optional count= field is present only when multiple PING events
2061    from the same sender to the same recipient, occurring within a
2062    coalescing window, have been merged into a single delivery
2063    (%-adversarial_behavior_4-%).  The value represents the number of
2064    individual PINGs coalesced.  When absent, the implicit value is 1.
2065    Clients SHOULD display count when greater than 1.
2066
2067    A single message may result in multiple PING events for the same
2068    recipient when the user is mentioned both directly and via one or
2069    more roles.  Clients MUST deduplicate such events based on mid, so
2070    that multiple PINGs referring to the same message produce a single
2071    notification.  In presentation, kind=user takes precedence over
2072    kind=role.  Clients MAY expose both forms in detailed views, but
2073    MUST treat them as a single notification for badge counts and unread
2074    indicators.
2075
2076    Receipt of a PING does not imply channel subscription.  Clients that
2077    require context MAY retrieve it explicitly, for example by issuing:
2078
2079      HIST cid=<cid> after=<mid> limit=1
2080
2081    Alternatively, the client may subscribe to the channel and then
2082    fetch history through standard mechanisms.
2083
2084    In direct-message contexts, a PING of kind=user is always generated
2085    for the recipient. No explicit mentions= tag is required, as the
2086    server infers the target from the DM resource.
2087
2088    Servers MUST persist PING events that cannot be delivered
2089    immediately to offline users.  Upon reconnect, all pending PINGs are
2090    delivered immediately after AUTH OK and before processing any
2091    client-issued verbs.  This ensures that clients can update unread
2092    indicators prior to receiving channel event streams.  Delivery order
2093    is strictly oldest-first.  Servers MAY expire undelivered PINGs
2094    after a server-defined retention period, with a minimum of 30 days
2095    RECOMMENDED.
2096
2097    A connected client MAY query pending PING events explicitly without
2098    relying on reconnect delivery. The request form is:
2099
2100      PING op=list [gid=<gid>] [cid=<cid>] [after=<ts-unix-ms>]
2101                    [offset=<n>] [limit=<n>]
2102
2103    All filters are optional and may be combined.  When gid= is present,
2104    results are restricted to channels within that community, and
2105    direct-message PINGs are excluded.  When gid= is absent, PINGs from
2106    all origins are returned.  Default offset is 0.  Default limit is
2107    50, with a maximum of 200; servers MUST enforce this upper bound.
2108
2109    The server responds with one PING event line per matching entry, in
2110    oldest-first order, followed by:
2111
2112      OK verb=PING op=list count=<n> total=<n>
2113
2114    The count field reflects the number of entries in the current page,
2115    while total reflects the total number of matching pending PINGs.
2116    The op=list operation does not consume or mark PINGs as delivered.
2117    Events returned by op=list are identical to those delivered on
2118    reconnect, and clients MUST deduplicate them by mid.
2119
2120    Clients dismiss a PING by issuing:
2121
2122      PING op=dismiss mid=<mid>
2123
2124    The server removes the corresponding (uid, mid) entry from the
2125    pending PING store.  In accordance with the commit-before-OK
2126    invariant (%-lineformat_commitbeforeok-%), this removal MUST occur
2127    before the server emits the OK response.  A subsequent PING op=list
2128    issued immediately after the OK MUST NOT include the dismissed mid.
2129
2130    The originating session receives:
2131
2132      OK verb=PING op=dismiss mid=<mid>
2133
2134    All other active sessions for the same uid receive a broadcast:
2135
2136      PING op=dismiss mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
2137
2138    The cid= field identifies the channel associated with the dismissed
2139    PING and is included to allow correct routing without additional
2140    lookup.  Receiving sessions MUST remove the corresponding entry from
2141    any displayed notification set.  This mechanism provides
2142    synchronization of notification state across multiple devices.
2143
2144    Dismissing a PING that is not present in the pending set, whether
2145    due to prior delivery, consumption, or expiration, MUST succeed
2146    silently and return OK.  The operation does not mark the underlying
2147    message as seen; advancing read state remains the responsibility of
2148    READSTATE op=mark (%-messages_readstate-%).
2149
2150    The PING store is normatively keyed by (uid, mid).  The cid
2151    component is not part of this key.  While %-identifiers-% requires
2152    that client state keyed by mid also include cid for message content,
2153    this rule does not apply to PING notification state.  PING state is
2154    scoped at the server level, where a given mid uniquely identifies a
2155    message. Accordingly, clients MUST key their PING notification store
2156    on mid alone within the context of the uid.
2157
2158    Clients that do not persist local state across sessions will receive
2159    the same pending PINGs on each reconnect until those events are
2160    explicitly dismissed.  Such clients MUST issue PING op=dismiss for
2161    each processed PING to prevent repeated delivery.  This represents
2162    the only case in the protocol where a stateless client is required
2163    to write state back to the server in order to maintain correct
2164    behavior.  The operation is lightweight and safe to perform
2165    unconditionally after display.
2166
2167    Mention storm protection is defined in %-adversarial_behavior-%.
2168
2169  %- messages_readstate = "Sect.  6.9" -%
2170  6.9  Read State (READSTATE)
2171
2172    Read state is maintained per (uid, cid, mid) tuple, with values of
2173    unseen (0) or seen (1), and a default of unseen.  The compound key
2174    (cid, mid) is required because mid is only unique within a channel
2175    (%-identifiers-%).  All READSTATE operations therefore use (cid,
2176    mid) as the storage key.  This differs from PING state
2177    (%-messages_ping-%), which is keyed solely by (uid, mid) due to its
2178    server-scoped semantics.
2179
2180    Servers maintain authoritative seen-state, while clients report
2181    transitions.  READSTATE is the single verb governing all read-state
2182    operations.
2183
2184    To mark an individual message as seen, the client issues:
2185
2186      READSTATE op=mark mid=<mid> cid=<cid>
2187
2188    The cid parameter is mandatory, as mid is not globally unique.  The
2189    server MUST use the (cid, mid) pair as the compound key when
2190    recording seen state, consistent with the %-identifiers-% invariant.
2191    If the specified mid is unknown to the server, with neither a live
2192    record nor a DEL tombstone, the server MUST return ERR
2193    code=NOTFOUND. If the message has been deleted but a DEL tombstone
2194    exists, the operation MUST succeed and return OK.  This allows
2195    clients to clear unread indicators for messages removed prior to
2196    viewing.  The commit-before-OK invariant
2197    (%-lineformat_commitbeforeok-%) applies.
2198
2199    The originating session receives:
2200
2201      OK verb=READSTATE op=mark mid=<mid> cid=<cid>
2202
2203    All other sessions of the same uid receive:
2204
2205      READSTATE op=mark mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
2206
2207    The cid field ensures correct routing of the update within the
2208    client’s read-state store.
2209
2210    Bulk marking is supported via:
2211
2212      READSTATE op=mark cid=<cid> upto=<mid>
2213
2214    This marks all messages in the specified channel with mid less than
2215    or equal to the supplied value as seen.  The server replies with OK
2216    to the originating session and broadcasts the update to other
2217    sessions of the same uid.
2218
2219    Clients may query read state at different granularities.  For a
2220    single channel:
2221
2222      READSTATE op=query cid=<cid>
2223
2224    The server responds with a summary including count of unseen
2225    messages, and optionally oldest and cursor markers, followed by OK.
2226    The count includes all message types in the channel.  In forum-style
2227    channels, this includes both thread openers and replies; therefore,
2228    count may remain non-zero even if only visible thread roots have
2229    been marked seen.
2230
2231    The oldest field identifies the earliest unseen message and MUST be
2232    present when count is non-zero.  The cursor field represents the
2233    highest mid marked seen and is omitted if no messages have been
2234    marked seen.  Both fields follow the standard tag absence semantics
2235    (%-lineformat_tagsemantics-%): missing tags indicate absence of
2236    value, not empty strings, and the literal 'none' MUST NOT be used.
2237
2238    The cursor is durable across reconnects and server restarts, as it
2239    is derived from persisted seen-state.  Clients typically use cursor
2240    as the after= parameter when issuing HIST following reconnect.  The
2241    absence of cursor has defined semantics.  If cursor is absent and
2242    count is zero, the channel is fully up to date.  If cursor is absent
2243    and count is non-zero, the channel is entirely unread.
2244
2245    READSTATE op=query gid=<gid> returns one line per channel in which
2246    the user has read state, defined as either a non-zero unseen count
2247    or a cursor.  Channels with no read-state entries are not included.
2248    As a result, clients MUST NOT infer channel existence from READSTATE
2249    responses and MUST query channel listings separately via COMM
2250    (%-identifiers-%).
2251
2252    Single-message queries are also supported:
2253
2254      READSTATE op=query mid=<mid> cid=<cid>
2255
2256    The server responds with seen=0 or seen=1, followed by OK.  The cid
2257    parameter is required for the same reason as in mark operations.
2258    The server MUST return ERR code=NOTFOUND if the specified (cid, mid)
2259    pair does not exist.  Deleted messages with tombstones remain valid
2260    targets for this query.
2261
2262    Reception of a PING does not alter read state.  Messages are marked
2263    seen only through explicit READSTATE operations initiated by the
2264    client.  Clients SHOULD mark messages as seen when they are
2265    presented to the user.
2266
2267    READSTATE events are not included in HIST responses.  Read state is
2268    user-private data and MUST NOT be broadcast to channel subscribers.
2269
2270    Edits do not affect seen state.  A message marked as seen remains so
2271    after subsequent edits.  Clients MAY choose to re-surface edited
2272    messages for user attention, but such behavior is a presentation
2273    concern rather than a protocol requirement.
2274
2275%- users_presence = "Sect.  7" -%
2276========================================================================
2277%-users_presence-% -- USERS AND PRESENCE
2278========================================================================
2279
2280  7.1  user info
2281
2282    WHO uid=<uid>
2283    server replies:
2284    PROFILE uid=<uid> name=<urlencoded-name> [avatar=<aid|https-url>]
2285             status=<status> ts=<unix-ms>
2286    OK verb=WHO uid=<uid>
2287    or
2288    ERR code=NOTFOUND :unknown uid
2289
2290    status values: online, idle, dnd, invisible, offline
2291
2292    full profile fetch (normative):
2293      WHO returns only the auth-layer identity fields (name=, avatar=, status=).
2294      For complete profile including social-layer fields, see %-section_7_4-% (full profile fetch).
2295
2296    invisible masking in PROFILE replies:
2297      if the queried uid's real status is invisible, the server MUST
2298      return status=offline in the PROFILE reply, EXCEPT when the
2299      requesting uid is the same as the queried uid (a user may always
2300      see his own real status). this is the same rule as for PRESENCE
2301      broadcasts (%-section_7_2-%): invisible is never exposed to other users.
2302
2303    PROFILE is the unified verb for both the WHO reply and the live
2304    broadcast pushed on IDENTIFY (%-section_4_5-%) and KEYROTATE (%-section_4_6-%). clients use PROFILE for all identity update handling regardless
2305    of whether they requested it or received it unsolicited.
2306
2307    PROFILE routing (normative):
2308      PROFILE is always server-sent; clients MUST NOT send PROFILE.
2309      parsers route PROFILE lines as follows:
2310        - if a WHO request is pending (terminal OK not yet received):
2311          the PROFILE line is the WHO reply; route it to that request.
2312        - if a PROFILE line carries prev_uid=: it is a KEYROTATE
2313          broadcast. no WHO is pending. clients MUST update their uid
2314          cache, merging old and new uid entries.
2315        - otherwise: it is an IDENTIFY broadcast (a user updated their
2316          display name or avatar). clients MUST update their uid cache.
2317      a PROFILE line with prev_uid= and a pending WHO for the same uid
2318      is a degenerate case; the KEYROTATE interpretation takes precedence
2319      and the WHO response will follow as a separate line or not at all
2320      (the uid may have changed). clients SHOULD re-issue WHO with the
2321      new uid in this case.
2322
2323  7.2  presence
2324
2325    PRESENCE status=online|idle|dnd|invisible
2326
2327    server broadcasts to clients subscribed to any channel shared with
2328    this user:
2329    PRESENCE uid=<uid> status=<status> ts=<unix-ms>
2330
2331    DM channel delivery scope (normative):
2332      DM cid subscriptions qualify as "shared channels" for PRESENCE
2333      broadcast delivery. a uid subscribed to a DM cid with another uid
2334      receives PRESENCE broadcasts for that uid, regardless of whether
2335      any community channel is shared between them. the DM channel
2336      establishes the relationship; the subscription activates delivery.
2337
2338    PRESENCE verb directionality (normative):
2339    parsers MUST use uid= presence to distinguish direction for the event
2340    form: a client-sent PRESENCE event line carries only status=; a
2341    server-broadcast PRESENCE event line carries uid= and ts= in addition
2342    to status=. servers MUST NOT send a bare PRESENCE with no uid=; clients
2343    MUST NOT send PRESENCE with uid=. this is the same normative rule used
2344    by EDIT (%-section_6_4-%), DEL (%-section_6_5-%), PIN (%-section_11_4-%), and TYPING
2345    (%-section_7_3-%). the query form (op=list) and its reply form (op=info) are
2346    exempt from this rule and are defined below.
2347
2348    servers MUST NOT send a reply (OK or ERR) to a valid PRESENCE event
2349    line (status=); PRESENCE events are fire-and-forget. clients MUST NOT
2350    wait for confirmation before considering the status change sent. the
2351    op=list query form is not an event and does receive an OK reply
2352    (see below).
2353
2354    presence state is ephemeral and connection-scoped:
2355      - servers MUST NOT persist presence state to durable storage.
2356      - on disconnect, server broadcasts PRESENCE uid=<uid> status=offline
2357        to affected subscribers and discards the entry.
2358      - the server-side presence store is a memory-only map from uid to
2359        current status, written on PRESENCE verb, deleted on disconnect.
2360      - servers MUST NOT attempt to restore presence state across restarts.
2361        a restarting server has no presence state; clients reconnect and
2362        re-declare their status.
2363      - after AUTH OK on reconnect, a client's presence entry is empty:
2364        the server has no record of the client's prior status. clients
2365        that wish to maintain a non-offline presence status after
2366        reconnection MUST re-send PRESENCE status=<status> after AUTH OK.
2367        a client that does not re-send PRESENCE after reconnect will
2368        appear offline to all other users until it does so. this is not
2369        an error; it is the correct behaviour of an ephemeral,
2370        connection-scoped store.
2371
2372    invisible status normative rules:
2373      - when a client sends PRESENCE status=invisible, the server MUST
2374        record the real status as invisible internally for this session.
2375      - the server MUST immediately broadcast PRESENCE uid=<uid>
2376        status=offline to all other subscribers (those sharing a channel
2377        with this uid). the uid appears offline to all other users.
2378      - the server MUST NOT transmit status=invisible to any connection
2379        other than the owning uid's own sessions (e.g. multi-device sync).
2380      - on disconnect of an invisible session, no additional broadcast is
2381        sent (the user was already seen as offline).
2382      - the 'here' built-in role (%-section_8_2-%) MUST treat invisible
2383        members as offline at PING fan-out resolution time. an invisible
2384        uid MUST NOT receive a PING via kind=role with rolid=here.
2385
2386    querying current presence (normative):
2387
2388      PRESENCE op=list gid=<gid>
2389
2390      returns the current non-offline status of all members of <gid>
2391      who have an active presence entry in the server's presence store.
2392      members with no entry or effective status=offline are omitted.
2393
2394      server replies with one line per qualifying member, then OK:
2395        PRESENCE op=info uid=<uid> status=<status> ts=<unix-ms>
2396        ...
2397        OK verb=PRESENCE op=list gid=<gid> count=<n> total=<n>
2398
2399      status values in op=info lines: online, idle, dnd. invisible
2400      masking applies: members with status=invisible are omitted (they
2401      appear offline to all other uids; same rule as broadcast masking
2402      above). ts= is the unix-ms of the most recent PRESENCE event
2403      received from that uid on the current connection. count= and
2404      total= are always equal (no pagination; the set is bounded by
2405      non-offline active connections to the community).
2406
2407      the result is best-effort: members may transition while the
2408      reply is in flight. clients MUST treat it as advisory, consistent
2409      with TYPING op=list (%-section_7_3-%).
2410
2411      servers MUST reject PRESENCE op=list with no gid= with
2412      ERR code=BADARG. servers MUST reject PRESENCE op=list for a
2413      gid= the requesting uid is not a member of with ERR code=NOPERM.
2414
2415      this query closes the bulk-read gap for PRESENCE state: WHO
2416      (%-section_7_1-%) is per-uid; op=list provides the reconnect snapshot
2417      needed to initialize presence state for community members without
2418      O(N) individual WHO requests. the best-effort caveat is
2419      acknowledged (see %-section_24-% rationale).
2420
2421  7.3  typing indicator
2422
2423    TYPING cid=<cid> state=start|stop
2424
2425    server broadcasts:
2426    TYPING cid=<cid> uid=<uid> state=start|stop ts=<unix-ms>
2427
2428    TYPING verb directionality (normative):
2429    parsers MUST use uid= presence to distinguish direction for the event
2430    form: a client-sent TYPING event line carries cid= and state= only; a
2431    server-broadcast TYPING event line carries uid= and ts= in addition.
2432    servers MUST NOT send a bare TYPING with no uid=; clients MUST NOT send
2433    TYPING with uid=. this is the same normative rule used by EDIT
2434    (%-section_6_4-%), DEL (%-section_6_5-%), PIN (%-section_11_4-%), and PRESENCE (%-section_7_2-%).
2435    the query form (op=list) and its reply form (op=info) are exempt from
2436    this rule and are defined below.
2437
2438    clients SHOULD debounce; send stop after 3 seconds of inactivity.
2439    clients SHOULD send stop before disconnecting.
2440    servers MUST NOT persist typing events.
2441    servers MUST NOT include typing events in HIST responses.
2442    servers MUST NOT send a reply (OK or ERR) to a valid TYPING event line
2443    (state=start|stop); TYPING events are fire-and-forget. clients MUST NOT
2444    wait for confirmation before considering the state change sent. the
2445    op=list query form is not an event and does receive an OK reply
2446    (see below).
2447
2448    typing state store:
2449      servers MUST maintain a memory-only per-cid typing state map:
2450        { cid -> { uid -> last_active_ts } }
2451      entries are added on state=start. entries are removed on state=stop
2452      or when the client disconnects. entries expire implicitly after the
2453      server-defined inactivity threshold (RECOMMENDED: 5 seconds). this
2454      store is the same pattern as PresenceStore (IMPL.md Sect. 23.5): never
2455      flushed to disk, empty on server restart.
2456
2457    querying current typing state:
2458
2459      TYPING op=list cid=<cid>
2460
2461      returns the currently-typing uids in cid as a best-effort snapshot.
2462      the result may be stale by the time the OK arrives (typing state
2463      expires within seconds). clients MUST treat this as advisory only.
2464
2465      server replies with one line per currently-typing uid, then OK:
2466        TYPING op=info cid=<cid> uid=<uid> ts=<unix-ms>
2467        ...
2468        OK verb=TYPING op=list cid=<cid> count=<n> total=<n>
2469
2470      ts= is the unix-ms of the most recent state=start received from
2471      that uid on this cid. count= and total= are always equal (no
2472      pagination; set is bounded by active channel subscribers per %-section_2_6-%).
2473
2474      this query closes the "no hidden state" design invariant for
2475      TYPING: typing state had a write path (state=start) but no read
2476      path at all. PRESENCE had a per-uid read path (WHO) but no bulk
2477      query; PRESENCE op=list (%-section_7_2-%) closes that analogous gap.
2478      the best-effort caveat is acknowledged (see %-section_24-% rationale).
2479
2480  7.4  user profile decoration
2481
2482    cap: kval. user profile decoration is served by the KVAL verb
2483    (%-section_7_5-%) under scope=user. see %-section_7_5-% for all wire mechanics,
2484    op= values, broadcast rules, and error semantics.
2485
2486    IDENTIFY (%-section_4_5-%) is the auth-layer verb that sets name= and
2487    avatar=. KVAL scope=user is the separate, orthogonal social-layer
2488    decoration: bio, banner image, accent color, and custom status text.
2489    the two layers share no fields and are queried independently.
2490
2491    defined keys for scope=user:
2492
2493      bio         -- wire markup (\n-encoded per %-section_2_4-%). the user's
2494                     freeform description. max 512 chars decoded. links
2495                     via [label](url) inline. clients render at L2+.
2496      banner      -- aid or https url (plain text). profile header image.
2497                     same format rules as community brand banner (%-section_7_5-%).
2498      color       -- 6-digit hex, no '#' prefix (e.g. '3b82f6'). accent
2499                     color used by clients for profile decoration.
2500      status_text -- plain text, 1-128 chars decoded. custom status message
2501                     displayed alongside presence status= (%-section_7_2-%).
2502                     distinct from presence status=; the status= field
2503                     carries the machine-readable state (online, idle, dnd,
2504                     invisible, offline); status_text= is freeform display.
2505
2506    permission rule for scope=user: a uid MAY only set keys on its own
2507    record. servers MUST reject KVAL op=set scope=user id=<other-uid>
2508    from a uid that is not <other-uid> with ERR code=NOPERM, unless the
2509    requesting uid holds server.admin. any authenticated uid may read
2510    any uid's scope=user record (same as WHO; no read restriction).
2511
2512    broadcast delivery for scope=user: server broadcasts the KVAL op=info
2513    event to all connections sharing at least one channel (community or
2514    DM) with the subject uid. this mirrors the PRESENCE delivery scope
2515    (%-section_7_2-%): the shared-channel relationship activates delivery.
2516
2517    full profile fetch (normative):
2518      WHO returns only the auth-layer fields (name=, avatar=, status=).
2519      KVAL op=get scope=user id=<uid> returns the social-layer fields.
2520      a client rendering a complete user profile card MUST issue both:
2521        WHO uid=<uid>
2522        KVAL op=get scope=user id=<uid>
2523      these may be pipelined; they produce responses on different verbs
2524      (PROFILE and KVAL respectively) and are self-routing. servers
2525      without cap=kval return ERR code=UNKNOWN on KVAL; clients MUST
2526      treat this as an absent social-layer profile and render with
2527      auth-layer fields only.
2528
2529  7.5  KVAL -- scoped key-value store
2530
2531    KVAL is a generic, scope-typed key-value store primitive. it provides
2532    one uniform verb for all named-field stores in the protocol, replacing
2533    per-entity ad-hoc key/value designs. the scope= tag identifies which
2534    store is addressed; the id= tag identifies the specific entity within
2535    that scope. op= selects the operation. the key= and payload carry the
2536    field name and value.
2537
2538    KVAL is governed by cap=kval. servers not advertising kval MUST
2539    return ERR code=UNKNOWN on all KVAL lines. clients MUST check cap=kval
2540    before sending any KVAL line.
2541
2542    7.5a  grammar
2543
2544      client sends (mutating):
2545        KVAL op=set   scope=<scope> id=<id> key=<key> :value
2546        KVAL op=del   scope=<scope> id=<id> key=<key>
2547
2548      client sends (query):
2549        KVAL op=get   scope=<scope> id=<id> [key=<key>]
2550
2551      server sends (response / event):
2552        KVAL op=info  scope=<scope> id=<id> key=<key> ts=<unix-ms> :value
2553        KVAL op=del   scope=<scope> id=<id> key=<key> ts=<unix-ms>
2554
2555      terminal OKs:
2556        OK verb=KVAL op=set  scope=<scope> id=<id> key=<key>
2557        OK verb=KVAL op=del  scope=<scope> id=<id> key=<key>
2558        OK verb=KVAL op=get  scope=<scope> id=<id> [key=<key>]
2559                             [count=<n> total=<n>]   (key=-absent form only)
2560
2561      scope   -- one of the defined scope names (%-section_7_5b-%). unknown scopes
2562                 MUST be rejected with ERR code=BADARG :unknown kval scope.
2563      id      -- the entity id within the scope. MUST be a valid id for
2564                 that scope type (uid for scope=user; gid for scope=comm).
2565                 servers MUST reject a mismatched id type with ERR
2566                 code=BADARG :id type mismatch for scope.
2567      key     -- one of the defined keys for this scope (%-section_7_5b-%). unknown
2568                 keys MUST be rejected with ERR code=BADARG :unknown key
2569                 for scope. key names use [a-z_], 1-32 chars.
2570      value   -- payload, encoded per %-section_2_4-%. format constraints
2571                 are per-key and listed in %-section_7_5b-%.
2572
2573    KVAL verb directionality (normative):
2574      parsers MUST use op= value to distinguish direction:
2575        client-sent:  op=set, op=get, op=del
2576        server-sent:  op=info, op=del (broadcast of a deletion)
2577      op=del appears in both directions with different tag sets:
2578        client-sent op=del: scope=, id=, key= only; no ts=.
2579        server-sent op=del: scope=, id=, key=, ts= (the commit time).
2580      servers MUST NOT send op=set or op=get. clients MUST NOT send
2581      op=info. servers MUST reject client-sent KVAL op=info with
2582      ERR code=BADARG. the ts= tag is the reliable discriminant for
2583      op=del direction: its presence means server-sent, its absence
2584      means client-sent.
2585
2586    7.5b  scope registry
2587
2588      the following scopes are defined. each entry specifies: the scope
2589      name, the id type, the valid key set with value constraints, the
2590      write permission rule, and the broadcast delivery scope.
2591
2592      scope=user
2593        id type:   uid
2594        keys:      see %-section_7_4-% (bio, banner, color, status_text)
2595        write:     uid may only set/del its own record (id == own uid).
2596                   server.admin may set/del any uid's record.
2597        read:      any authenticated uid. no restriction.
2598        broadcast: all connections sharing at least one channel
2599                   (community or DM) with the subject uid.
2600        key limit: 4 defined keys maximum.
2601
2602      scope=comm
2603        id type:   gid
2604        keys:      see %-section_8_6-% (description, icon, banner, color, rules)
2605        write:     requires channel.manage or community.admin on the gid.
2606                   servers MUST reject KVAL op=set scope=comm key=name; the
2607                   display name is owned by COMM op=update (%-section_8_0-%).
2608        read:      any member of the community. non-members of private
2609                   communities receive ERR code=NOPERM.
2610        broadcast: all members of the community (gid).
2611        key limit: 5 defined keys maximum.
2612
2613      scope=mute
2614        id type:   uid
2615        keys:      mute_channels   (comma-separated cids where notification
2616                                    sounds/alerts are suppressed but info
2617                                    remains visible in UI)
2618                   mute_users      (comma-separated uids whose messages
2619                                    never trigger notification sounds/alerts
2620                                    but remain visible in UI)
2621                   mute_roles      (comma-separated rolids: if a message
2622                                    sender holds any listed role, suppress
2623                                    notification sound/alert but remain visible)
2624                   ignore_channels (comma-separated cids to hide from UI
2625                                    completely; PINGs are generated but not
2626                                    rendered)
2627                   ignore_users    (comma-separated uids to hide from UI
2628                                    completely; messages are received but not
2629                                    rendered)
2630                   ignore_roles    (comma-separated rolids: hide messages
2631                                    from senders holding any listed role)
2632        write:     uid may only set/del its own record (id == own uid).
2633                   server.admin may set/del any uid's record.
2634        read:      self only. non-self reads MUST return ERR code=NOPERM.
2635        broadcast: all sessions of the subject uid (multi-device sync).
2636                   KVAL op=info for scope=mute MUST NOT broadcast to
2637                   connections of other uids.
2638        key limit: 6 defined keys maximum.
2639
2640      adding a new scope in a future revision requires only a new entry
2641      in this registry and a new id type binding. no verb changes are
2642      needed. unknown scopes are rejected at the server; forward-compat
2643      clients that check cap=kval before sending are unaffected.
2644
2645    7.5c  op=set
2646
2647      KVAL op=set scope=<scope> id=<id> key=<key> :value
2648
2649      sets or overwrites the value for key on the identified entity.
2650      commit-before-OK invariant (%-section_2_6-%) applies: the server MUST commit
2651      the new value before sending OK.
2652
2653      server replies to the originating client:
2654        OK verb=KVAL op=set scope=<scope> id=<id> key=<key>
2655
2656      server broadcasts to the delivery scope for this scope (%-section_7_5b-%):
2657        KVAL op=info scope=<scope> id=<id> key=<key> ts=<unix-ms> :value
2658
2659      ts= is the unix-ms at which the server committed the set.
2660
2661    7.5d  op=del
2662
2663      KVAL op=del scope=<scope> id=<id> key=<key>
2664
2665      deletes a key from the identified entity's record. after deletion,
2666      KVAL op=get for that key returns ERR code=NOTFOUND.
2667
2668      server replies to the originating client:
2669        OK verb=KVAL op=del scope=<scope> id=<id> key=<key>
2670
2671      server broadcasts to the delivery scope for this scope (%-section_7_5b-%):
2672        KVAL op=del scope=<scope> id=<id> key=<key> ts=<unix-ms>
2673
2674      deleting a key that does not exist MUST return OK silently (no
2675      broadcast). this is idempotent: a client that retries a del after
2676      a dropped connection will not cause a spurious broadcast.
2677
2678    7.5e  op=get
2679
2680      KVAL op=get scope=<scope> id=<id> [key=<key>]
2681
2682      queries the current value(s) for the identified entity.
2683
2684      single-key form (key= present):
2685        server replies with one KVAL op=info line, then OK:
2686          KVAL op=info scope=<scope> id=<id> key=<key> ts=<unix-ms> :value
2687          OK verb=KVAL op=get scope=<scope> id=<id> key=<key>
2688        or, if the key has not been set:
2689          ERR code=NOTFOUND :key not set
2690
2691      all-keys form (key= absent):
2692        server replies with one KVAL op=info line per set key, then OK:
2693          KVAL op=info scope=<scope> id=<id> key=<key> ts=<unix-ms> :value
2694          ...
2695          OK verb=KVAL op=get scope=<scope> id=<id> count=<n> total=<n>
2696        count and total are always equal; the key set is bounded by the
2697        scope's key limit (%-section_7_5b-%). if no keys have been set, the server
2698        returns zero KVAL op=info lines then OK count=0 total=0.
2699        ERR code=NOTFOUND is NOT returned for the all-keys form; it is
2700        reserved for the single-key form only.
2701
2702      KVAL op=info is listed in %-section_2_5-% as a routable response verb.
2703      parsers route KVAL op=info lines via pending-request state.
2704      the (scope, id) pair in each line makes all-keys responses
2705      self-routing when multiple KVAL op=get requests are pipelined
2706      for different (scope, id) pairs.
2707
2708    7.5f  pipelining
2709
2710      KVAL op=get requests for different (scope, id) pairs MAY be
2711      pipelined. each KVAL op=info response line carries both scope=
2712      and id=, making it self-routing without pending-request state.
2713      the same-resource pipeline prohibition (%-section_2_5-%) applies at the
2714      (scope, id) granularity: clients MUST NOT pipeline two KVAL
2715      op=get requests for the same (scope, id) pair.
2716
2717    7.5g  ts= semantics
2718
2719      KVAL follows the %-section_2_6-% ts= invariant unconditionally. ts= in KVAL
2720      op=info response lines is the unix-ms of the most recent op=set
2721      that produced the current value (last-modified time). ts= in KVAL
2722      op=del broadcast lines is the unix-ms of the commit that deleted
2723      the key (event commit time). no deviation from %-section_2_6-% exists for
2724      KVAL; this subsection is a convenience pointer only.
2725
2726    7.5h  scope=mute client enforcement (normative)
2727
2728      scope=mute stores user notification preferences. servers do NOT
2729      suppress PING generation or message delivery; all PINGs and messages
2730      are sent normally. clients MUST fetch and cache all mute/ignore keys
2731      on startup (KVAL op=get scope=mute id=<self-uid>). clients MUST NOT
2732      request scope=mute for other uids; servers MUST reject with ERR
2733      code=NOPERM.
2734
2735      when a client processes a PING or message, it MUST check mute and
2736      ignore conditions:
2737        - if the message's cid is in mute:channels, suppress notification
2738          sound and desktop alert
2739        - if the message's sender uid is in mute:users, suppress notification
2740          sound and desktop alert
2741        - if the sender's roles include any rolid in mute:roles, suppress
2742          notification sound and desktop alert
2743        - if the message's cid is in ignore:channels, mark as ignored
2744        - if the message's sender uid is in ignore:users, mark as ignored
2745        - if the sender's roles include any rolid in ignore:roles, mark as
2746          ignored
2747
2748      mute preference behavior is normative: suppress all notification sounds
2749      and desktop alerts. message rendering is client-side presentation.
2750
2751      ignore preference behavior is client-side presentation only. servers
2752      do not enforce ignore state. clients MAY render ignored messages
2753      collapsed (e.g. "<N> ignored message<s>"), expandable on click, or
2754      apply any other presentation choice. ignored messages remain fully
2755      received and available; the client chooses how to display them.
2756
2757      mute/ignore preference changes (KVAL op=info and op=del broadcasts)
2758      take effect immediately on all devices when received. a client
2759      receiving scope=mute broadcast updates MUST refresh its mute/ignore
2760      cache and apply the new preferences to subsequent PINGs.
2761
2762%- community_management = "Sect.  8" -%
2763========================================================================
2764%-community_management-% -- COMMUNITY MANAGEMENT
2765========================================================================
2766
2767  community management uses the COMM verb with an op= tag for all
2768  operations: lifecycle (creation, deletion, joining, leaving, discovery)
2769  and administration (roles, members, channels, branding). this keeps
2770  the surface orthogonal: one verb, one tag selects the operation.
2771
2772  8.0  community lifecycle
2773
2774    a community does not exist until COMM op=create is issued on the
2775    wire. there is no out-of-band creation path. servers MUST NOT
2776    require an HTTP API, dashboard, or other side-channel to bring a
2777    community into existence. the protocol is self-contained.
2778
2779    this mirrors the IRC /JOIN-to-create idiom but makes the semantics
2780    explicit and separates creation from subscription (SUB), which is
2781    a channel-level concept.
2782
2783    COMM op=create name=<urlencoded-name> [slug=<slug>] [open=0|1]
2784
2785      name    -- display name; percent-encoded; 1-64 chars decoded.
2786      slug    -- community slug; see %-section_3-% for grammar and
2787                 derivation rules. [a-z0-9_-], 1-32 chars.
2788                 if omitted, server derives slug from name (%-section_3-%).
2789                 server MUST reject a duplicate slug with ERR code=CONFLICT.
2790      open    -- 1: any authenticated user may COMM op=join without a
2791                    code. 0 (default): join requires a valid invite code.
2792
2793      server replies:
2794      OK verb=COMM op=create gid=<gid> slug=<slug>
2795         default_cid=<cid> default_slug=<slug>
2796
2797      gid          -- the new community's ULID.
2798      slug         -- the canonical community slug (server-derived if
2799                      not supplied).
2800      default_cid  -- cid of the auto-created general channel.
2801      default_slug -- slug of the auto-created general channel. clients
2802                      can immediately construct the channel's full rid:
2803                      /<hostname>/<community-slug>/<default_slug>
2804
2805      the creating user is automatically a member and is assigned the
2806      built-in 'owner' role (%-section_8_0a-%). no further COMM op=join
2807      is needed. the server MUST atomically create a default general
2808      channel of type=text and MUST assign it to this community.
2809
2810      server broadcasts to all sessions of the creating uid:
2811      MEMBER op=join gid=<gid> uid=<uid> ts=<unix-ms> role=owner
2812
2813    COMM op=delete gid=<gid> :reason
2814
2815      permanently deletes the community, all its channels, messages,
2816      roles, member records, and KVAL scope=comm keys for the gid.
2817      reason payload is required.
2818
2819      requires owner role. servers MUST reject with ERR code=NOPERM
2820      if the requesting uid does not hold the owner role.
2821
2822      server replies to the acting uid FIRST:
2823      OK verb=COMM op=delete gid=<gid>
2824
2825      after sending OK, the server broadcasts to all current members
2826      including the acting uid:
2827      MEMBER op=delete gid=<gid> by=<acting-uid> ts=<unix-ms> :reason
2828
2829      the acting uid receives both the OK and the MEMBER op=delete
2830      broadcast; broadcasts to the acting uid are not suppressed. this
2831      follows the universal OK-before-broadcast rule (see MSG, EDIT, DEL).
2832
2833      after broadcasting, the server closes all active subscriptions
2834      on channels belonging to gid and removes all member records.
2835      the gid is retired; servers MUST NOT reuse it.
2836
2837      in-flight queries during COMM op=delete (normative):
2838        if a HIST query is in progress on a channel belonging to the
2839        gid at the time op=delete is processed, the server MUST
2840        complete the HIST response (emit all buffered event lines and
2841        the terminal OK) before closing the subscription. the client
2842        will subsequently receive MEMBER op=delete and have its
2843        subscription dropped. servers MUST NOT terminate a HIST stream
2844        mid-response with an ERR due to concurrent community deletion.
2845
2846    COMM op=join gid=<gid> [code=<invitecode>]
2847
2848      joins the authenticated user to the community.
2849
2850      if open=1 was set at creation time, code MAY be omitted.
2851      if open=0, code is required; servers MUST reject without a valid
2852      code with ERR code=NOPERM :invite required.
2853
2854      code is validated for expiry, use-count, and membership uniqueness.
2855      COMM op=join with a code is the sole canonical form of invite
2856      redemption.
2857
2858      server replies:
2859      OK verb=COMM op=join gid=<gid>
2860
2861      server broadcasts to all current members:
2862      MEMBER op=join gid=<gid> uid=<uid> ts=<unix-ms>
2863
2864      the joining user is NOT automatically subscribed to any channel.
2865      he must issue SUB for each channel he wishes to receive messages
2866      from. membership and subscription are orthogonal: membership
2867      governs permissions; subscription governs event delivery.
2868
2869      note: after joining, a client that wishes to find available
2870      channels MUST issue COMM op=channel.list gid=<gid> (%-section_8_4-%).
2871      the server does not push channel information automatically on join.
2872
2873    COMM op=leave gid=<gid>
2874
2875      the authenticated user voluntarily leaves the community.
2876      an owner MUST NOT leave if he is the sole remaining owner; server
2877      rejects with ERR code=FORBIDDEN :transfer or delete community first.
2878
2879      server replies:
2880      OK verb=COMM op=leave gid=<gid>
2881
2882      server broadcasts to all remaining members:
2883      MEMBER op=leave gid=<gid> uid=<uid> ts=<unix-ms>
2884
2885      all active subscriptions the leaving user holds on channels
2886      belonging to gid are silently dropped by the server.
2887
2888    COMM op=update gid=<gid> [name=<urlencoded-name>] [open=0|1]
2889
2890      updates the community display name, open/invite-only flag, or
2891      both. at least one of name= or open= MUST be present; servers
2892      MUST reject a COMM op=update with neither with ERR code=BADARG.
2893      requires community.admin or owner role.
2894
2895      name    -- new display name; percent-encoded; 1-64 chars decoded.
2896                 the community slug is NOT changed by a name update.
2897                 slugs are stable identifiers (%-section_3-%).
2898      open    -- 1: any authenticated user may join without a code.
2899                 0: join requires a valid invite code.
2900
2901      server replies:
2902      OK verb=COMM op=update gid=<gid>
2903
2904      server broadcasts to all current members:
2905      COMM op=update gid=<gid> uid=<acting-uid>
2906           [name=<urlencoded-name>] [open=0|1] ts=<unix-ms>
2907
2908      COMM op=update verb directionality (normative):
2909      parsers MUST use uid= presence to distinguish direction: a client-
2910      sent COMM op=update line carries gid= and the update tags (name=
2911      and/or open=) but NO uid=; a server-broadcast COMM op=update line
2912      carries uid= and ts= in addition. servers MUST NOT broadcast COMM
2913      op=update without uid=; clients MUST NOT send COMM op=update with
2914      uid=. note: COMM op=update is the only community entity operation
2915      where the management verb (COMM) also serves as the broadcast verb.
2916      this is an intentional design exception documented in %-section_24-%; the uid=
2917      discriminant makes the direction unambiguous without a separate verb.
2918
2919      servers MUST include in the broadcast ONLY the tags that were
2920      present in the send form (name= and/or open=); tags not submitted
2921      in the send form MUST be omitted from the broadcast. clients MUST
2922      update their local community cache on receiving this broadcast.
2923      a tag absent from the broadcast means that field is unchanged;
2924      clients MUST NOT clear or reset fields for absent tags.
2925
2926      note on branding vs. display name: COMM op=update name= is the
2927      canonical community display name used in COMM op=info and membership
2928      flows. key=name is not valid in KVAL scope=comm (see %-section_8_6-%).
2929      there is exactly one source of truth for the community display name.
2930
2931    COMM op=list [offset=<n>] [limit=<n>] [q=<urlencoded-query>]
2932
2933      enumerates all communities hosted on this server that match:
2934      - open communities (open=1), or
2935      - communities of which the requesting uid is a member.
2936      all matching communities are included in the result (paginated).
2937
2938      offset default: 0. limit default: 20. limit max: 100.
2939      servers MUST clamp limit to 100.
2940      q is an optional server-side substring filter on community name
2941      and slug.
2942
2943      server replies with one COMM line per result:
2944      COMM op=info gid=<gid> slug=<slug> name=<urlencoded-name>
2945           open=0|1 members=<n> owner=<uid> ts=<unix-ms>
2946      ...
2947      OK verb=COMM op=list count=<n> total=<n>
2948
2949      count: number of lines in this page.
2950      total: total matching communities across all pages (for pagination).
2951
2952    COMM op=info gid=<gid>
2953
2954      queries metadata for one community. any authenticated user may
2955      query an open community; private communities return ERR
2956      code=NOPERM unless the requester is a member.
2957
2958      server replies:
2959      COMM op=info gid=<gid> slug=<slug> name=<urlencoded-name>
2960           open=0|1 members=<n> owner=<uid> ts=<unix-ms>
2961      OK verb=COMM op=info gid=<gid>
2962
2963  8.0a  built-in role: owner
2964
2965    in addition to 'everyone' and 'here' (%-section_8_2-%), each community
2966    has a third immutable built-in role with the reserved literal rolid
2967    'owner'.
2968
2969      'owner'  -- held by the community creator and any uid to whom
2970                  ownership has been explicitly transferred. there MUST
2971                  be exactly one owner at all times; the server enforces
2972                  this invariant on all role.assign rolid=owner,
2973                  COMM op=leave, and COMM op=role.revoke operations.
2974
2975                  the owner role implies ALL permissions unconditionally,
2976                  including community.admin. it cannot be dropped from a
2977                  permission resolution by any deny.
2978
2979                  ownership transfer:
2980                    COMM op=role.assign gid=<gid> uid=<target-uid>
2981                         rolid=owner
2982                  transfers ownership to target-uid. the acting uid
2983                  MUST currently hold owner. the server atomically
2984                  assigns owner to target-uid and revokes it from the
2985                  acting uid; there is never a moment with two owners or
2986                  zero owners. servers MUST reject if the acting uid
2987                  does not hold owner with ERR code=NOPERM.
2988
2989                  'owner' appears in COMM op=role.list responses.
2990                  it cannot be renamed, deleted, or have its permissions
2991                  modified. COMM ops targeting it for those purposes
2992                  MUST be rejected with ERR code=BADARG.
2993
2994  8.0b  MEMBER broadcast
2995
2996    MEMBER is the community membership event verb. it is sent to all
2997    current members of a community whenever membership changes.
2998
2999      MEMBER op=join gid=<gid> uid=<uid> ts=<unix-ms> [role=<rolid>]
3000        -- a user joined (voluntarily or via invite).
3001           role is present only for op=join at creation time (role=owner).
3002
3003      MEMBER op=leave gid=<gid> uid=<uid> ts=<unix-ms>
3004        -- a user left voluntarily (COMM op=leave).
3005
3006      MEMBER op=kick gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms> :reason
3007        -- a user was kicked (COMM op=member.kick; %-section_8_3-%).
3008           by= carries the moderator's uid. reason payload is the
3009           stated reason, relayed verbatim from the COMM op=member.kick.
3010
3011      MEMBER op=ban gid=<gid> uid=<uid> by=<acting-uid>
3012             dur=<seconds> ts=<unix-ms> :reason
3013        -- a user was banned (COMM op=member.ban; %-section_8_3-%).
3014           dur=0 means permanent. reason payload is the stated reason.
3015
3016      MEMBER op=timeout gid=<gid> uid=<uid> by=<acting-uid>
3017             dur=<seconds> ts=<unix-ms> :reason
3018        -- a user was timed out (COMM op=member.timeout; %-section_8_3-%).
3019           reason payload is the stated reason.
3020
3021      MEMBER op=unban gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms>
3022        -- a ban was lifted (COMM op=member.unban; %-section_8_3-%).
3023
3024      MEMBER op=untimeout gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms>
3025        -- an active timeout was explicitly lifted before natural expiry
3026           (COMM op=member.untimeout; %-section_8_3-%). no payload: an
3027           explicit lift carries no required reason.
3028
3029      MEMBER op=delete gid=<gid> by=<acting-uid> ts=<unix-ms> :reason
3030        -- the community itself was deleted (COMM op=delete).
3031           by= carries the uid of the owner who issued the delete.
3032
3033    MEMBER event delivery scope (normative):
3034      MEMBER events are delivered only to connections with at least one
3035      active channel subscription in the gid. a uid that is a community
3036      member but has zero active channel subscriptions in that community
3037      will NOT receive live MEMBER events (op=join, op=leave, op=kick,
3038      op=ban, op=timeout, op=unban, op=untimeout, op=delete) for that
3039      community. this is a fundamental invariant of the protocol:
3040      subscription scope equals event delivery scope.
3041
3042      dormant members (no active subscriptions) MUST recover membership
3043      state by querying on reconnect. the normative recovery sequence:
3044        1. COMM op=info gid=<gid>         -- verify community exists;
3045                                             ERR code=NOTFOUND if deleted.
3046        2. COMM op=member.list gid=<gid>  -- full current member set with roles,
3047                                             banned_until=, timeout_until=
3048        3. COMM op=role.list gid=<gid>    -- current role definitions
3049
3050      if COMM op=info returns ERR code=NOPERM, the community became
3051      private and the member was removed; clients MUST purge local state
3052      for the gid. if COMM op=info returns OK, the member's presence in
3053      COMM op=member.list confirms current membership; absence indicates
3054      the member was kicked (removed with no ongoing record).
3055      clients MUST NOT assume cached membership state is current without
3056      verifying via query after any period of zero subscriptions in a
3057      community. the AUDIT extension (%-section_25-%) is the long-term path for
3058      historical join/leave/ban queries; it is not defined in 0.8.
3059
3060    MEMBER events are NOT included in HIST responses (normative).
3061      MEMBER events are gid-scoped; HIST is cid-scoped. including them
3062      would conflate two distinct scopes and break the per-cid ordering
3063      model. this exclusion is intentional and permanent for the HIST verb.
3064
3065  8.1  community structure
3066
3067    communities contain: channels, forums, roles, members, branding.
3068    all management operations use the COMM verb with an op= tag.
3069
3070  8.2  roles and permissions
3071
3072    ROLE is the single verb for all role query replies and live-update
3073    broadcasts. op= distinguishes the kind of event. this mirrors CHAN
3074    and EMOJI: one verb, op= selects meaning, no separate event verb.
3075
3076    COMM op=role.create gid=<gid> name=<urlencoded-name>
3077         [color=<hex6>] [emoji=<eid|cldr-name>] [hoist=0|1]
3078         [perms=<permset>]
3079
3080      server replies:
3081      OK verb=COMM op=role.create gid=<gid> rolid=<rolid>
3082
3083      rolid is the server-assigned ULID for the new role. the client
3084      MUST use this rolid for all subsequent operations on the role.
3085
3086      server broadcasts to all community members:
3087      ROLE op=create gid=<gid> rolid=<rolid> name=<urlencoded-name>
3088           [color=<hex6>] [emoji=<eid|cldr-name>] [hoist=0|1]
3089           [perms=<permset>] ts=<unix-ms>
3090
3091      clients receiving ROLE op=create MUST add the role to their
3092      local role cache so subsequent assign/revoke events are meaningful
3093      without a follow-up COMM op=role.list round-trip.
3094
3095    COMM op=role.rename gid=<gid> rolid=<rolid> name=<urlencoded-name>
3096
3097      renames a user-defined role. built-in roles (owner, everyone,
3098      here) MUST be rejected with ERR code=BADARG.
3099      requires role.manage permission.
3100
3101      server replies:
3102      OK verb=COMM op=role.rename gid=<gid> rolid=<rolid>
3103
3104      server broadcasts to all community members:
3105      ROLE op=rename gid=<gid> rolid=<rolid> name=<urlencoded-name>
3106           ts=<unix-ms>
3107
3108    COMM op=role.perm gid=<gid> rolid=<rolid> [perms=<permset>]
3109
3110      replaces the permission set of a user-defined role. built-in
3111      roles (owner, everyone, here) MUST be rejected with ERR code=BADARG.
3112      requires role.manage permission.
3113
3114      perms= is the full new permission set for the role (same format
3115      as the perms= tag on role.create). the server replaces the stored
3116      set atomically; it is not additive. to clear all permissions, omit
3117      the perms= tag entirely; a tag with an empty value violates the
3118      1*TCHAR rule of %-section_2_1-%. servers MUST treat an absent perms=
3119      as meaning "clear all permissions" in this context.
3120
3121      server replies:
3122      OK verb=COMM op=role.perm gid=<gid> rolid=<rolid>
3123
3124      server broadcasts to all community members:
3125      ROLE op=perm gid=<gid> rolid=<rolid> [perms=<permset>] ts=<unix-ms>
3126
3127      perms= is omitted in the broadcast when the role's permission set
3128      was cleared; its absence means the role now has no permissions.
3129      clients MUST treat absent perms= in ROLE op=perm as clearing the
3130      cached permission set for that role, not as "unchanged".
3131
3132      clients MUST update their local role cache on receiving ROLE op=perm.
3133      the op=perm event is listed in the full op= enumeration in %-section_8_2a-% alongside all other ROLE event kinds.
3134
3135    COMM op=role.assign gid=<gid> uid=<uid> rolid=<rolid>
3136
3137      server replies:
3138      OK verb=COMM op=role.assign gid=<gid> uid=<uid> rolid=<rolid>
3139
3140      server broadcasts to community members:
3141      ROLE op=assign gid=<gid> rolid=<rolid> uid=<uid> ts=<unix-ms>
3142
3143    COMM op=role.revoke gid=<gid> uid=<uid> rolid=<rolid>
3144
3145      server replies:
3146      OK verb=COMM op=role.revoke gid=<gid> uid=<uid> rolid=<rolid>
3147
3148      server broadcasts to community members:
3149      ROLE op=revoke gid=<gid> rolid=<rolid> uid=<uid> ts=<unix-ms>
3150
3151    COMM op=role.delete gid=<gid> rolid=<rolid>
3152
3153      permanently removes a user-defined role. built-in roles (owner,
3154      everyone, here) MUST be rejected with ERR code=BADARG.
3155      all role assignments for this rolid are removed atomically.
3156
3157      server replies:
3158      OK verb=COMM op=role.delete gid=<gid> rolid=<rolid>
3159
3160      server broadcasts to community members:
3161      ROLE op=delete gid=<gid> rolid=<rolid> ts=<unix-ms>
3162
3163      clients MUST remove the role from their local role cache on
3164      receiving ROLE op=delete.
3165
3166    built-in roles:
3167
3168      every community has three immutable built-in roles with reserved
3169      literal rolids. they cannot be created, renamed, or deleted.
3170      'owner' is defined authoritatively in %-section_8_0a-%. the two
3171      community-wide implicit-membership roles:
3172
3173      'everyone'   -- implicit membership: all current community members.
3174                      rolid literal is the string 'everyone'.
3175                      appears in rolmentions=, ROLE events, and
3176                      permission grants like any other role.
3177                      membership is dynamically resolved server-side;
3178                      no explicit assignment needed or permitted.
3179
3180      'here'       -- implicit membership: all members currently online
3181                      in the community (presence status != offline).
3182                      rolid literal is the string 'here'.
3183                      membership is resolved at PING fan-out time.
3184                      servers MUST check 'here' membership at PING
3185                      dispatch time, not at MSG validation time. a member
3186                      who transitions to offline or invisible between MSG
3187                      validation and async PING dispatch (see %-section_26_1-%,
3188                      PING_FANOUT_ASYNC) MUST NOT receive the PING for
3189                      that dispatch.
3190                      a member who goes offline between MSG arrival and
3191                      PING dispatch is skipped for 'here'.
3192
3193      built-in roles are listed in COMM op=role.list responses.
3194      their appearance is configurable via COMM op=role.appearance
3195      like any other role.
3196      COMM op=role.assign and role.revoke are rejected for 'everyone'
3197      and 'here' with ERR code=BADARG (membership is implicit and
3198      dynamic). for 'owner' assignment and revocation rules see
3199      %-section_8_0a-%.
3200
3201    canonical perms= for built-in roles (normative):
3202
3203      servers MUST include perms= in ROLE op=info and COMM op=role.list
3204      responses for built-in roles as follows:
3205
3206        owner     -- perms= tag MUST be omitted entirely. the owner
3207                     bypass is structural (all permissions implied
3208                     unconditionally) and cannot be expressed as a
3209                     permset. an empty perms= value would violate the
3210                     1*TCHAR rule of %-section_2_1-%. clients receiving a
3211                     ROLE op=info for rolid=owner MUST treat the absence
3212                     of perms= as meaning "all permissions implied".
3213
3214        everyone  -- perms=msg.send,attach.upload,react.use,
3215                            board.post,thread.create
3216                     these are the default permissions granted to all
3217                     community members. servers MAY extend this set via
3218                     configuration; the listed tokens are the normative
3219                     minimum baseline.
3220
3221        here      -- perms=msg.send,attach.upload,react.use,
3222                            board.post,thread.create
3223                     identical to everyone. 'here' is a fan-out
3224                     membership filter, not a separate permission scope.
3225                     its permission set is always derived from and
3226                     identical to 'everyone'; it cannot be independently
3227                     configured. the distinction between them is delivery
3228                     scope only: @everyone mentions all community members;
3229                     @here mentions only currently online members. a role
3230                     with a deny on 'here' perms has no effect that is
3231                     not already achieved by a deny on 'everyone' perms.
3232                     clients MUST treat here's permission set as identical
3233                     to everyone's.
3234
3235    user-defined roles:
3236
3237      rolids are server-assigned ULIDs, assigned at role.create time.
3238      names are URL-encoded, 1-64 chars decoded.
3239
3240    permset: comma-separated list of permission tokens.
3241    defined tokens:
3242      msg.send         msg.delete       msg.pin
3243      member.ban       member.kick      member.timeout   member.untimeout
3244      channel.manage   role.manage      community.admin
3245      attach.upload    react.use        thread.create    thread.manage
3246      board.post       board.manage     invite.create    invite.manage
3247      emoji.manage     mention.role
3248      mention.everyone mention.here
3249
3250    community.admin super-permission rule (normative):
3251      community.admin is a super-permission that implies all other
3252      permission tokens unconditionally. if a uid holds community.admin in
3253      any role assigned to it, that uid has all other permissions on all
3254      channels in the community regardless of their role assignments or
3255      channel.perm denials. per-token implications listed below describe
3256      the expected semantic relationship; all such implications reduce to
3257      this master rule in the permission resolution algorithm.
3258
3259      msg.send:
3260        required to send any MSG in the channel. the server MUST
3261        reject MSG from a uid without msg.send with ERR code=NOPERM.
3262        granted to 'everyone' by default on text, forum, and board
3263        channels. communities that want a read-only channel (equivalent
3264        to the former type=announce) MUST deny msg.send for the 'everyone'
3265        role via COMM op=channel.perm on a type=text channel.
3266
3267      msg.delete:
3268        required to delete or edit messages authored by others.
3269        a user may always delete or edit his own messages regardless
3270        of this permission.
3271        note: on type=board channels, board.manage REPLACES this
3272        permission for actions on other users' content. msg.delete is
3273        NOT evaluated for others' posts on board channels; see board.manage.
3274
3275      msg.pin:
3276        required to PIN or UNPIN any message in the channel.
3277        note: on type=board channels, board.manage REPLACES this
3278        permission for pin actions on other users' content; see board.manage.
3279
3280      attach.upload:
3281        required to include aid= references in message bodies.
3282        servers MUST strip aid= markup from MSG bodies sent by users
3283        without this permission and report WARN code=ATTACH_STRIPPED.
3284        granted to 'everyone' by default.
3285
3286      react.use:
3287        required to send REACT or UNREACT on the channel.
3288        granted to 'everyone' by default.
3289
3290      thread.create:
3291        on text channels: required to start a new thread (send MSG
3292        with tid= set to a new value --- a tid= not referencing an
3293        existing thread). note: on forum channels, creating a thread
3294        is controlled by msg.send; thread.create is not evaluated.
3295        granted to 'everyone' by default on text channels.
3296
3297      thread.manage:
3298        required to rename or lock a thread (%-section_11_2a-%).
3299        a thread's original author may rename or lock his own thread
3300        regardless of this permission.
3301
3302      board.post:
3303        required to create a new top-level post (MSG with title=) on
3304        a type=board channel. granted to 'everyone' by default.
3305        msg.send is also required on board channels (it is the universal
3306        MSG gate checked first in the permission resolution algorithm,
3307        step 3); board.post is an additional gate on top of msg.send.
3308        a uid that holds board.post but not msg.send is rejected at the
3309        msg.send check before board.post is evaluated.
3310
3311      board.manage:
3312        required to edit or delete posts authored by others on a
3313        type=board channel, and to pin posts on board channels.
3314
3315        on type=board channels, board.manage REPLACES msg.delete and
3316        msg.pin for actions on other users' content. msg.delete and
3317        msg.pin are NOT evaluated for others' posts on board channels;
3318        only board.manage is checked. a user without board.manage but
3319        with msg.delete will be rejected on a board channel when
3320        attempting to delete another user's post.
3321
3322        a user's own posts follow the universal self-edit rule: any
3323        author may edit or delete his own posts on any channel type
3324        regardless of msg.delete or board.manage.
3325
3326      channel.manage:
3327        required to execute COMM op=channel.create, channel.delete,
3328        channel.rename, channel.topic, channel.perm for channels within
3329        this community.
3330
3331      role.manage:
3332        required to execute COMM op=role.create, role.rename,
3333        role.perm, role.assign, role.revoke, role.delete,
3334        role.appearance for this community.
3335
3336      mention.role:
3337        required to use rolmentions= on a MSG at all.
3338        without it, the entire rolmentions= tag is dropped and the
3339        sender receives WARN code=MENTION_ROLE_NOPERM :rolmentions
3340        requires mention.role permission.
3341
3342      mention.everyone:
3343        required to include the built-in rolid 'everyone' in rolmentions=.
3344        attempting rolmentions=everyone without this permission causes
3345        ERR code=NOPERM (hard reject, not a silent drop).
3346
3347      mention.here:
3348        required to include the built-in rolid 'here' in rolmentions=.
3349        attempting rolmentions=here without this permission causes
3350        ERR code=NOPERM (hard reject, not a silent drop).
3351
3352      the asymmetry between mention.role (WARN + drop) and
3353      mention.everyone / mention.here (ERR + reject) exists because
3354      fan-out for built-in roles may be community-wide; the cost must
3355      be explicitly rejected, not quietly discarded.
3356
3357      per-role mention control:
3358        to restrict which specific user-defined roles a user may mention,
3359        use the channel.perm mechanism to deny rolmentions on a per-role
3360        basis. mention.role, mention.everyone, and mention.here are the
3361        only normative mention permission tokens; there are no per-rolid
3362        mention tokens.
3363
3364    permission resolution algorithm:
3365
3366      given a user U and a permission P on channel C in community G:
3367
3368      step 1: collect all roles assigned to U in G, including built-in
3369              roles ('everyone' is always assigned to all members;
3370              'here' is assigned to online members at eval time).
3371
3372      step 2: if ANY of U's roles has community.admin in its allow set:
3373                grant P unconditionally. stop.
3374
3375      step 3: evaluate community-level role permissions:
3376                for each role R assigned to U:
3377                  if P is in R.deny: DENY. stop.
3378                if any role R has P in R.allow: ALLOW. proceed to step 4.
3379                else: DENY (no channel override can grant). stop.
3380
3381      step 4: if C has a channel-level perm override for any of U's roles:
3382                for each role R with a channel override:
3383                  if P is in channel-deny for R: DENY. stop.
3384                if P is in channel-allow for any R: ALLOW. stop.
3385                // no channel override: community result from step 3 stands
3386
3387      summary: deny-wins at each layer. community.admin beats all.
3388      channel-level overrides cannot grant a permission denied at
3389      community level (except by community.admin).
3390
3391  8.2a  role appearance
3392
3393    roles carry visual metadata used by clients for rendering mentions,
3394    member lists, and role badges. appearance is set with:
3395
3396    COMM op=role.appearance gid=<gid> rolid=<rolid>
3397         [color=<hex6>] [emoji=<eid|cldr-name>] [hoist=0|1]
3398
3399    color  -- 6-digit hex foreground color for this role's name display.
3400              applied when rendering @rolid:name mentions in markup.
3401              also used for member list coloring.
3402    emoji  -- a single emoji (custom eid or CLDR name) used as a role
3403              badge/icon. clients display this beside the role name.
3404    hoist  -- 1 means show this role as a separate group in member lists.
3405              0 means members appear in the default group. default 0.
3406
3407    server replies:
3408    OK verb=COMM op=role.appearance gid=<gid> rolid=<rolid>
3409
3410    server broadcasts to community members on appearance change:
3411    ROLE op=appearance gid=<gid> rolid=<rolid>
3412         [color=<hex6>] [emoji=<eid|cldr-name>]
3413         [hoist=0|1] ts=<unix-ms>
3414
3415    ROLE is the single verb for all role events and query replies.
3416    op= distinguishes the kind:
3417      op=info        -- reply to COMM op=role.list query (read-only)
3418      op=create      -- broadcast: role created
3419      op=rename      -- broadcast: role renamed
3420      op=perm        -- broadcast: role permission set changed
3421      op=appearance  -- broadcast: role appearance changed
3422      op=assign      -- broadcast: role assigned to a member
3423      op=revoke      -- broadcast: role revoked from a member
3424      op=delete      -- broadcast: role deleted
3425
3426    clients SHOULD cache role appearance per (gid, rolid).
3427    on SUB to a community channel, server MAY proactively push the full
3428    role list as ROLE op=info lines (same format as COMM op=role.list)
3429    before the SUB OK, so clients can warm their role cache without a
3430    separate round-trip. this mirrors the emoji push in %-section_10_4-%.
3431    clients receiving these pre-SUB pushes MUST process them identically
3432    to live ROLE op=info responses.
3433
3434    ROLE op=info pre-SUB-OK push routing (normative):
3435      ROLE op=info lines arriving before a pending SUB OK are cache-warm
3436      push lines. parsers MUST route them by gid= to the role cache for
3437      that community. the pending SUB request provides the frame context;
3438      %-section_2_5-% pending-request state covers these lines as part of the SUB
3439      response. no additional discriminant tag is required.
3440
3441    cache-warm push principle (normative):
3442      servers MAY proactively push, before a SUB OK, entity data required
3443      for rendering incoming MSG lines in the subscribed channel. in 0.8
3444      this means: ROLE op=info lines (required for mention display), EMOJI
3445      op=info lines (required for emoji display), and CHAN op=perm lines
3446      (required for permission state; see %-section_8_4-%). servers MUST NOT
3447      proactively push entity data not required for message rendering ---
3448      specifically, branding (KVAL scope=comm), member lists (MEMBER op=info), or
3449      channel lists (CHAN op=info) --- as pre-SUB-OK unsolicited data.
3450      clients query those on demand. this boundary is intentional:
3451      unbounded pre-SUB pushes would block the client parser for an
3452      unspecified duration on every subscribe.
3453
3454    fetching role list and appearance:
3455    COMM op=role.list gid=<gid> [offset=<n>] [limit=<n>]
3456
3457    offset default: 0. limit default: 50. limit max: 200.
3458    servers MUST clamp limit to 200.
3459
3460    server replies with one ROLE line per role, then OK:
3461
3462    ROLE op=info gid=<gid> rolid=<rolid> name=<urlencoded-name>
3463         [color=<hex6>] [emoji=<eid|cldr-name>] [hoist=0|1]
3464         [perms=<permset>] ts=<unix-ms>
3465    ...
3466    OK verb=COMM op=role.list gid=<gid> count=<n> total=<n>
3467
3468    count: number of lines in this page. total: total roles in gid.
3469
3470    ROLE op=info dual-direction use (normative):
3471      ROLE op=info appears in three server-sent contexts:
3472        (a) as the per-line reply to COMM op=role.list (%-section_8_2a-%).
3473        (b) as the proactive cache-warm push before SUB OK (%-section_8_2a-%).
3474        (c) as the single-line reply to COMM op=role.info (see below).
3475      in all three contexts the line format is identical. parsers MUST
3476      handle ROLE op=info in all contexts with one handler; routing is
3477      via %-section_2_5-% pending-request state.
3478      ROLE op=info is NEVER sent as an unsolicited live-event broadcast;
3479      all unsolicited live events use the specific op= values listed above
3480      (op=create, op=rename, op=perm, op=appearance, op=assign, op=revoke,
3481      op=delete). this is the same design used by CHAN and EMOJI.
3482
3483    single-role lookup:
3484
3485    COMM op=role.info gid=<gid> rolid=<rolid>
3486
3487      queries metadata for a single role by rolid. any community member
3488      may call this. non-members of private communities receive ERR
3489      code=NOPERM.
3490
3491      server replies:
3492      ROLE op=info gid=<gid> rolid=<rolid> name=<urlencoded-name>
3493           [color=<hex6>] [emoji=<eid|cldr-name>] [hoist=0|1]
3494           [perms=<permset>] ts=<unix-ms>
3495      OK verb=COMM op=role.info gid=<gid> rolid=<rolid>
3496      or
3497      ERR code=NOTFOUND :role not found in this community
3498
3499      this closes the gap where a ROLE op=assign broadcast for an
3500      unknown rolid forced a full COMM op=role.list round-trip. mirrors
3501      COMM op=channel.info cid=<cid> exactly.
3502
3503  8.3  member management
3504
3505    COMM op=member.ban       gid=<gid> uid=<uid> [dur=<seconds>] :reason
3506    COMM op=member.kick      gid=<gid> uid=<uid> :reason
3507    COMM op=member.timeout   gid=<gid> uid=<uid> dur=<seconds> :reason
3508    COMM op=member.unban     gid=<gid> uid=<uid>
3509    COMM op=member.untimeout gid=<gid> uid=<uid>
3510
3511    dur=0 on ban means permanent. dur= absent in COMM op=member.ban is
3512    equivalent to dur=0: a ban with no explicit duration is permanent.
3513    servers MUST emit dur=0 in the MEMBER op=ban broadcast when dur= was
3514    absent in the send form; the broadcast always carries an explicit value.
3515    servers MUST reject COMM op=member.timeout with dur=0 with
3516    ERR code=BADARG :timeout duration must be positive. a zero-duration
3517    timeout expires immediately and is semantically void; use
3518    COMM op=member.ban dur=0 for a permanent restriction.
3519    reason payload is required for ban and kick; SHOULD be present
3520    for timeout. reason is stored in mod log. servers MUST persist
3521    ban/timeout state with expiry.
3522
3523    owner protection (normative):
3524      servers MUST reject COMM op=member.kick, op=member.ban, and
3525      op=member.timeout targeting a uid that holds the owner role,
3526      with ERR code=FORBIDDEN :owner role is protected. ownership must
3527      be transferred (%-section_8_0a-%) before the owner can be restricted.
3528
3529    ban and timeout membership semantics (normative):
3530      ban and timeout do NOT remove the uid's member record. a banned
3531      uid remains listed in COMM op=member.list and is queryable via
3532      COMM op=member.info; the reply includes banned_until= or
3533      timeout_until= as appropriate (see MEMBER op=info below).
3534      subscriptions are removed on ban and timeout (%-section_5-% GC rules);
3535      the member record itself is retained.
3536
3537      a banned uid is rejected with ERR code=NOPERM on all write
3538      operations: MSG, REACT, UNREACT, PIN, UNPIN, EDIT, DEL, and
3539      channel management verbs. a timed-out uid is similarly rejected.
3540      read operations (COMM op=info, COMM op=channel.list, WHO) are
3541      permitted for banned members of open communities.
3542      COMM op=leave MUST be accepted from a banned or timed-out uid;
3543      a restricted member may remove themselves voluntarily.
3544      COMM op=join MUST be rejected for a currently banned uid with
3545      ERR code=NOPERM :you are banned.
3546
3547      because ban removes all channel subscriptions (%-section_5-% GC),
3548      banned members receive no channel broadcasts for the duration of
3549      the ban. the member record is retained for administrative query.
3550
3551    server replies for each operation:
3552    OK verb=COMM op=member.ban       gid=<gid> uid=<uid>
3553    OK verb=COMM op=member.kick      gid=<gid> uid=<uid>
3554    OK verb=COMM op=member.timeout   gid=<gid> uid=<uid>
3555    OK verb=COMM op=member.unban     gid=<gid> uid=<uid>
3556    OK verb=COMM op=member.untimeout gid=<gid> uid=<uid>
3557
3558    corresponding broadcasts to community members (%-section_8_0b-%):
3559    MEMBER op=ban       gid=<gid> uid=<uid> by=<acting-uid> dur=<s> ts=<unix-ms> :reason
3560    MEMBER op=kick      gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms> :reason
3561    MEMBER op=timeout   gid=<gid> uid=<uid> by=<acting-uid> dur=<s> ts=<unix-ms> :reason
3562    MEMBER op=unban     gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms>
3563    MEMBER op=untimeout gid=<gid> uid=<uid> by=<acting-uid> ts=<unix-ms>
3564
3565    the reason payload is relayed verbatim in the broadcast so that all
3566    subscribed members (including moderator tooling and audit clients) can
3567    log the stated reason without a separate query. MEMBER op=unban and
3568    MEMBER op=untimeout carry no payload because an explicit lift has no
3569    required reason.
3570    ban reasons are stored server-side for mod-log purposes. in 0.8 there
3571    is no protocol query for stored mod-log history; retrieval is reserved
3572    for the AUDIT extension (%-section_25-%).
3573
3574    natural expiry of timed bans and timeouts (normative):
3575      when a ban (dur>0) or timeout expires, the server lifts the
3576      restriction silently. no broadcast is sent for server-side expiry;
3577      there is no MEMBER op=unban or op=untimeout event for natural expiry.
3578      clients MUST derive the expiry time from dur= and ts= in the original
3579      MEMBER op=ban or MEMBER op=timeout broadcast:
3580        expiry_unix_ms = ts + (dur * 1000)
3581      a client that has not received the broadcast (e.g. was offline)
3582      discovers expiry implicitly: COMM op=member.info for the uid
3583      succeeds normally once the restriction has lifted. COMM op=member.unban
3584      and COMM op=member.untimeout are for explicit admin lifts only;
3585      they return OK silently if no restriction is currently active.
3586      servers MUST NOT emit a MEMBER op=unban or MEMBER op=untimeout
3587      broadcast when returning OK silently on a no-op lift; a broadcast
3588      would misrepresent a no-op as a live moderation event. this is the
3589      same normative pattern as PIN idempotency (%-section_11_4-%).
3590
3591    COMM op=member.list gid=<gid> [offset=<n>] [limit=<n>]
3592
3593    enumerates members of a community. any community member may call
3594    this. non-members of private communities receive ERR code=NOPERM.
3595    bulk enumeration concerns are addressed by rate limiting (%-section_18-%), not
3596    by permission gating: COMM op=member.info (single-uid lookup) already
3597    returns identical fields including banned_until= and timeout_until= to
3598    any community member; the list form exposes no additional data.
3599    offset default: 0. limit default: 50. limit max: 200.
3600    servers MUST clamp limit to 200.
3601
3602    server replies with one MEMBER op=info line per member, then OK:
3603    MEMBER op=info gid=<gid> uid=<uid> name=<urlencoded-name>
3604           [roles=<csv-rolid>] joined=<unix-ms>
3605           [banned_until=<unix-ms|permanent>] [timeout_until=<unix-ms>]
3606           ts=<unix-ms>
3607    ...
3608    OK verb=COMM op=member.list gid=<gid> count=<n> total=<n>
3609
3610    roles= is a comma-separated list of user-defined rolids explicitly
3611    assigned to this member, plus 'owner' if the member holds that role.
3612    the tag is OMITTED entirely when the member has no assigned roles
3613    (a tag with an empty value violates the 1*TCHAR rule of %-section_2_1-%).
3614    built-in roles 'everyone' and 'here' are never included; their
3615    membership is implicit and dynamic.
3616    joined= is the unix-ms at which the member joined the community.
3617    ts= is the unix-ms of the most recent mutation to this member record
3618    (role assignment/revoke, ban, timeout, unban, or untimeout). equals
3619    joined= if no mutation has occurred since joining. this follows the
3620    %-section_2_6-% ts= invariant for op=info replies.
3621    banned_until=permanent when a dur=0 (permanent) ban is active.
3622    banned_until=<unix-ms> when a timed ban is active (expiry timestamp).
3623    timeout_until=<unix-ms> when a timeout is active (expiry timestamp).
3624    both tags are absent when no restriction is in effect.
3625    COMM op=member.list includes banned and timed-out members; callers
3626    identify restricted members by the presence of banned_until= or
3627    timeout_until= in MEMBER op=info lines.
3628
3629    COMM op=member.info gid=<gid> uid=<uid>
3630
3631    queries a single member by uid. requires community membership;
3632    non-members receive ERR code=NOPERM on private communities.
3633    server replies:
3634    MEMBER op=info gid=<gid> uid=<uid> name=<urlencoded-name>
3635           [roles=<csv-rolid>] joined=<unix-ms>
3636           [banned_until=<unix-ms|permanent>] [timeout_until=<unix-ms>]
3637           ts=<unix-ms>
3638    OK verb=COMM op=member.info gid=<gid> uid=<uid>
3639    or
3640    ERR code=NOTFOUND :uid is not a member of this community
3641
3642    NOTFOUND is returned for uids that are not members (i.e. never
3643    joined, or were kicked). banned members are still members and
3644    return MEMBER op=info with banned_until= present.
3645
3646    kick vs ban record semantics (normative):
3647      kick and ban have deliberately different effects on the member record:
3648
3649        ban:  the member record is retained. the uid remains queryable via
3650              COMM op=member.info with banned_until= present. the ban is
3651              visible protocol state. a banned uid may be unbanned by
3652              COMM op=member.unban without creating a new member record.
3653
3654        kick: the member record is removed entirely. subsequent COMM
3655              op=member.info for the kicked uid returns ERR code=NOTFOUND,
3656              indistinguishable from a uid that has never joined. if the
3657              kicked uid re-joins via COMM op=join, a fresh member record
3658              is created with a new joined= timestamp. the fact that the
3659              uid was previously a member and was kicked is NOT recoverable
3660              from 0.8 protocol state.
3661
3662      this asymmetry is intentional: a ban is an ongoing restriction
3663      (a uid status) and MUST be readable state. a kick is an instantaneous
3664      removal with no ongoing restriction; retaining a tombstone would
3665      require defining its lifecycle (query surface, expiry, cleanup) with
3666      no corresponding protocol benefit in 0.8.
3667
3668      moderation history (who kicked whom, when, and why) is available
3669      only via the AUDIT extension (%-section_25-%). in 0.8 there is no
3670      protocol path to distinguish a kicked former member from a uid that
3671      never joined. clients and operators that require this distinction
3672      MUST use out-of-band logging or wait for the AUDIT extension.
3673
3674    MEMBER op=info disambiguation (normative):
3675      MEMBER op=info lines appear in two server-sent contexts: as the
3676      per-line body of COMM op=member.list responses, and as the single-
3677      line reply to COMM op=member.info. within the same gid=, no tag
3678      discriminant distinguishes them; parsers MUST route MEMBER op=info
3679      lines using %-section_2_5-% pending-request state. pipelining COMM op=member.list
3680      with COMM op=member.info for the same gid= is subject to the same-
3681      resource pipeline prohibition (%-section_2_5-%); serialize them.
3682      COMM op=member.list and COMM op=member.info for DIFFERENT gid=
3683      values MAY be pipelined: each MEMBER op=info line carries gid= and
3684      uid=, making it self-routing to the correct pending request by
3685      (gid, uid) without pending-request state ambiguity.
3686
3687  8.4  channels
3688
3689    channel management uses COMM ops for write operations and the CHAN
3690    verb for both query responses and live-update broadcasts. the pattern
3691    mirrors ROLE and EMOJI: CHAN op=info is a query reply;
3692    CHAN op=create/delete/rename/topic/perm are broadcast events.
3693
3694    gid+cid co-validation (normative):
3695      for all COMM ops that accept both gid= and cid=
3696      (channel.delete, channel.rename, channel.topic, channel.perm):
3697      servers MUST verify that the supplied cid belongs to the community
3698      identified by gid. a mismatch MUST return ERR code=BADARG :cid
3699      does not belong to this community. this prevents permission
3700      resolution running against the wrong community context and makes
3701      gid= a routing guarantee, not merely advisory context.
3702
3703    COMM op=channel.create gid=<gid> name=<urlencoded-name>
3704         type=text|voice|forum|board [slug=<slug>] [e2e=0|1]
3705
3706      name    -- display name; percent-encoded; 1-64 chars decoded.
3707      slug    -- channel slug; see %-section_3-% for grammar and derivation.
3708                 [a-z0-9_-], 1-32 chars, unique within the community.
3709                 if omitted, server derives slug from name (%-section_3-%).
3710                 server MUST reject a duplicate slug with ERR code=CONFLICT.
3711
3712      server replies:
3713      OK verb=COMM op=channel.create gid=<gid> cid=<cid> slug=<slug>
3714
3715      server broadcasts to all community members:
3716      CHAN op=create gid=<gid> cid=<cid> slug=<slug> name=<urlencoded-name>
3717           type=text|voice|forum|board e2e=0|1 uid=<acting-uid> ts=<unix-ms>
3718
3719      channels are created with no topic. to set a topic, issue
3720      COMM op=channel.topic after creation (see below).
3721
3722    COMM op=channel.delete gid=<gid> cid=<cid>
3723
3724      server replies:
3725      OK verb=COMM op=channel.delete gid=<gid> cid=<cid>
3726
3727      server broadcasts to all community members:
3728      CHAN op=delete gid=<gid> cid=<cid> uid=<acting-uid> ts=<unix-ms>
3729
3730      all clients subscribed to the deleted cid have their subscription
3731      silently dropped by the server immediately after the broadcast.
3732      clients receiving CHAN op=delete MUST remove the channel from their
3733      local channel list and MAY display a "channel deleted" notice.
3734
3735    COMM op=channel.rename gid=<gid> cid=<cid> name=<urlencoded-name>
3736
3737      renames a channel's display name. the channel slug is NOT changed
3738      by a rename; slugs are stable identifiers (%-section_3-%). to change
3739      the slug, delete and recreate the channel.
3740      requires channel.manage permission.
3741
3742      name    -- new display name; percent-encoded; 1-64 chars decoded.
3743
3744      server replies:
3745      OK verb=COMM op=channel.rename gid=<gid> cid=<cid>
3746
3747      server broadcasts to all community members:
3748      CHAN op=rename gid=<gid> cid=<cid> name=<urlencoded-name>
3749           uid=<acting-uid> ts=<unix-ms>
3750
3751    COMM op=channel.topic gid=<gid> cid=<cid> :topic (wire markup, \n-encoded)
3752
3753      updates the channel topic. the topic payload is wire markup per
3754      %-section_9-%, \n-encoded per %-section_2_4-%.
3755      requires channel.manage permission.
3756
3757      clearing the topic: sending COMM op=channel.topic with an empty
3758      payload (the line ends with ' :' followed immediately by LF, per
3759      %-section_2_1-%) clears the topic. the server MUST store no topic for
3760      the channel and MUST broadcast CHAN op=topic with an empty payload.
3761      clients MUST treat an empty payload in CHAN op=topic as a clear-
3762      topic event and remove any cached topic for this cid. subsequent
3763      CHAN op=info and COMM op=channel.info replies will carry no topic
3764      payload (no ' :' delimiter on the line).
3765
3766      server replies:
3767      OK verb=COMM op=channel.topic gid=<gid> cid=<cid>
3768
3769      server broadcasts to all community members:
3770      CHAN op=topic gid=<gid> cid=<cid> uid=<uid> ts=<unix-ms>
3771           :new topic   (empty payload when topic is cleared)
3772
3773    COMM op=channel.perm gid=<gid> cid=<cid> rolid=<rolid>
3774         [allow=<permset>] [deny=<permset>]
3775
3776      sets per-channel permission overrides for a role. allow= and deny=
3777      are each independently optional; a tag absent from the send form
3778      means no override in that direction (and clears any existing
3779      override for that direction). sending with neither allow= nor deny=
3780      present clears all channel-level overrides for the role entirely.
3781      a tag with an empty value violates the 1*TCHAR rule of %-section_2_1-%
3782      and MUST NOT be sent; use tag absence to express "no override".
3783      the broadcast mirrors this: allow= or deny= is omitted when that
3784      direction carries no override.
3785
3786      server replies:
3787      OK verb=COMM op=channel.perm gid=<gid> cid=<cid> rolid=<rolid>
3788
3789      server broadcasts to all community members:
3790      CHAN op=perm gid=<gid> cid=<cid> rolid=<rolid>
3791           [allow=<permset>] [deny=<permset>] uid=<acting-uid> ts=<unix-ms>
3792
3793      CHAN op=perm verb directionality (normative):
3794      parsers MUST use uid= presence to distinguish direction: a client-sent
3795      COMM op=channel.perm line is a management command (COMM verb) and
3796      produces no client-sent CHAN line. a server-broadcast CHAN op=perm line
3797      carries uid= and ts=; a query-reply CHAN op=perm line (from COMM
3798      op=channel.perm.list, see below) carries ts= but MUST NOT carry uid=.
3799      clients MUST treat uid= absent in a CHAN op=perm line as a query reply,
3800      not a live event. this is the same normative pattern used by ATTACH
3801      (%-section_12_3-%).
3802
3803      ts= semantic (normative):
3804      ts= in ALL CHAN op=perm lines --- both broadcast and query-reply --- is
3805      defined as: the unix-ms at which the most recent COMM op=channel.perm
3806      for this (cid, rolid) pair was committed by the server. for a broadcast
3807      line, this equals the wall time of the event (since the broadcast IS
3808      the commit). for a query-reply line, this is the stored last-modified
3809      time for the override. the two values are always identical: the server
3810      stores the commit time when it processes the channel.perm command and
3811      returns the same value on query. implementors MUST use a single ts=
3812      field with this unified semantic; no second timestamp field is needed.
3813
3814    COMM op=channel.perm.list gid=<gid> cid=<cid>
3815
3816      queries the current per-channel role permission overrides for a
3817      channel. any community member may query. non-members of private
3818      communities receive ERR code=NOPERM.
3819
3820      this query closes the hidden-state gap: channel-level overrides set
3821      by COMM op=channel.perm are broadcast as CHAN op=perm events, but a
3822      reconnecting client that missed those broadcasts has no way to
3823      reconstruct the override state without this query surface. every other
3824      persistent entity in the protocol has a query surface; channel perm
3825      overrides are no exception.
3826
3827      server replies with one CHAN op=perm line per role that has any
3828      override (allow= and/or deny= set), then OK:
3829      CHAN op=perm gid=<gid> cid=<cid> rolid=<rolid>
3830           [allow=<permset>] [deny=<permset>] ts=<unix-ms>
3831      ...
3832      OK verb=COMM op=channel.perm.list gid=<gid> cid=<cid> count=<n> total=<n>
3833
3834      ts= in each CHAN op=perm line is the unix-ms of the most recent
3835      COMM op=channel.perm for that (cid, rolid) pair.
3836      count: number of lines returned. no pagination: the number of roles
3837      per channel is bounded by the community's role count.
3838      if no overrides are set for any role on this channel: zero CHAN op=perm
3839      lines, then OK count=0 total=0. ERR code=NOTFOUND is NOT returned for the
3840      zero-override case.
3841
3842      servers MAY proactively push CHAN op=perm lines before the SUB OK
3843      as part of the cache-warm burst (see %-section_8_2a-% cache-warm push principle).
3844      these pre-SUB-OK CHAN op=perm lines carry ts= but no uid=, identical
3845      to the COMM op=channel.perm.list reply format.
3846
3847    COMM op=channel.list gid=<gid> [offset=<n>] [limit=<n>]
3848
3849      enumerates all channels in a community. any community member may
3850      query. non-members of private communities receive ERR code=NOPERM.
3851
3852      offset default: 0. limit default: 50. limit max: 200.
3853      servers MUST clamp limit to 200.
3854      (note: COMM op=list has a lower default of 20 because it enumerates
3855      server-wide communities; a server may host many communities. channel
3856      counts are bounded by a single community's design; 50 is the
3857      appropriate default for intra-community enumeration.)
3858
3859      server replies with one CHAN op=info line per channel, then OK.
3860      topic (wire markup) is the line payload; absent if not set:
3861
3862      CHAN op=info gid=<gid> cid=<cid> slug=<slug> name=<urlencoded-name>
3863           type=text|voice|forum|board e2e=0|1 ts=<unix-ms>
3864           [:topic]
3865      ...
3866      OK verb=COMM op=channel.list gid=<gid> count=<n> total=<n>
3867
3868      count: number of lines in this page. total: total channels in gid.
3869      ts is the unix-ms of the last modification to the channel.
3870      the payload carries the current topic as wire markup (\n-encoded
3871      per %-section_2_4-%). if no topic has been set the payload is absent
3872      (no ' :' delimiter on the line).
3873
3874      channel ordering (normative):
3875        COMM op=channel.list returns channels in ascending cid (ULID)
3876        order. because cids are server-assigned ULIDs whose embedded
3877        timestamp reflects the channel creation time, ULID order is
3878        canonical creation order. this gives clients a stable, predictable
3879        channel list order without requiring a separate position field.
3880        clients MUST NOT assume any other ordering and MUST use the
3881        list order as the authoritative display order unless the user
3882        has applied an explicit local sort. user-controlled channel
3883        ordering (e.g. drag-and-drop reorder) is not defined in 0.8;
3884        see %-section_25-% (future extensions) for CHAN op=order.
3885
3886    COMM op=channel.info cid=<cid>
3887
3888      queries metadata for a single channel by cid. any member of the
3889      community that owns the channel may query. non-members of private
3890      communities MUST receive ERR code=NOPERM, consistent with every
3891      other community query verb.
3892
3893      server replies:
3894      CHAN op=info gid=<gid> cid=<cid> slug=<slug> name=<urlencoded-name>
3895           type=text|voice|forum|board e2e=0|1 ts=<unix-ms>
3896           [:topic]
3897      OK verb=COMM op=channel.info cid=<cid>
3898
3899    typical reconnect flow:
3900
3901      // 1. authenticate; server restores subscriptions and sends pending PINGs
3902      //    automatically --- no SUB re-issuance needed for previously subscribed
3903      //    channels. PING events arrive before further processing.
3904
3905      // 2. if client state was lost entirely (no local community/subscription cache):
3906      //    recover community membership and state via normative recovery sequence:
3907      COMM op=list              // all joined communities (paginated, default limit=20)
3908      COMM op=member.list gid=<gid>  // for each joined gid from op=list
3909      COMM op=role.list gid=<gid>    // for each joined gid from op=list
3910
3911      // 3. for each subscribed or rejoined community, check read state:
3912      READSTATE op=query gid=<gid>
3913      // note: a brand-new member who has never marked any message seen will
3914      // receive zero READSTATE lines before the OK. this is correct and means
3915      // the entire channel history is unread (cursor=absent, count=0 means
3916      // up-to-date per %-section_6_9-%; cursor=absent, count>0 means fully unread).
3917      // to distinguish "gid has channels" from "gid has no channels", the client
3918      // MUST cross-reference with COMM op=channel.list gid=<gid>. the READSTATE
3919      // response alone is insufficient for a first-time member.
3920
3921      // 4. fetch history from last read cursor on each subscribed channel:
3922      HIST cid=<cid> after=<cursor> limit=50
3923      // first-time load (no stored cursor): bare HIST returns newest messages
3924      HIST cid=<cid> limit=50
3925
3926      // 5. to inspect current subscription set:
3927      SUB op=list
3928
3929      // 6. a first-time client (no prior subscriptions) discovers channels and
3930      //    subscribes:
3931      COMM op=channel.list gid=<gid>
3932      SUB cid=<cid>   // durable: server will restore this on next connect
3933
3934      // for dormant members that lost all state:
3935      // on reconnect, run steps 2-3 above. if COMM op=list returns a community
3936      // the client knew it was in but COMM op=member.list doesn't include the
3937      // client's uid, the member was kicked. if COMM op=info returns ERR
3938      // code=NOPERM, the community became private and the member was removed.
3939      // clients MUST purge state for such communities.
3940
3941    note: a storage-free client can reconstruct all uid-scoped state from
3942    server queries alone. no local storage is required for correct operation.
3943
3944  8.5  invites
3945
3946    COMM op=invite.create gid=<gid> [uses=<n>] [ttl=<seconds>]
3947    server replies:
3948    OK verb=COMM op=invite.create gid=<gid> code=<code>
3949       [ttl=<seconds>] [uses=<n>]
3950
3951    code= is an opaque server-generated string using characters [A-Za-z0-9],
3952    1-32 chars. clients MUST treat it as opaque; no structure is guaranteed.
3953
3954    uses= absent or omitted on invite.create means unlimited redemptions.
3955    uses=<n> (positive integer) sets a finite redemption cap.
3956    ttl= absent: no expiry (canonical form; servers MUST omit ttl= from
3957    the OK and from INVITE op=info lines when no expiry was set).
3958    ttl=0: MUST be rejected with ERR code=BADARG :ttl must be positive or
3959    absent. a zero ttl would expire immediately and is semantically void.
3960    this is parallel to the timeout dur=0 rejection (%-section_8_3-%).
3961    ttl=<n> (positive integer): seconds until expiry.
3962    the OK echoes ttl= and uses= when they were supplied, so callers
3963    can confirm the values the server stored without a follow-up query.
3964    both tags are absent in the OK when no expiry / unlimited redemptions.
3965    expired or exhausted codes return ERR code=GONE on use.
3966
3967    resolving a code to community metadata (pre-join lookup):
3968
3969      COMM op=invite.info code=<code>
3970
3971      any authenticated user may call this regardless of membership.
3972      server replies with the community metadata the code grants access to:
3973
3974      INVITE op=info gid=<gid> code=<code> name=<urlencoded-name>
3975             open=0|1 members=<n> [ttl=<seconds>] [uses=<n>] [used=<n>]
3976      OK verb=COMM op=invite.info code=<code>
3977
3978      gid=    the community this code grants entry to.
3979      name=   the community display name; sufficient to show a preview UI.
3980      open=   whether the community is open (1) or invite-only (0).
3981      members= current member count.
3982      ttl=    seconds remaining before expiry; absent if no expiry.
3983      uses=   max redemptions (positive integer); absent if unlimited.
3984      used=   number of times already redeemed.
3985
3986      if the code is expired, exhausted, or does not exist:
3987      ERR code=GONE :invite code is invalid or expired
3988
3989      this is the correct path for any flow where the gid is not already
3990      known --- e.g. a user clicks an invite link that carries only the code.
3991      the client calls COMM op=invite.info to learn the gid and show a
3992      community preview, then calls COMM op=join gid=<gid> code=<code>
3993      to redeem.
3994
3995    to redeem a code:
3996      COMM op=join gid=<gid> code=<code>   (%-section_8_0-%)
3997
3998  8.6  community branding
3999
4000    community branding is served by the KVAL verb (%-section_7_5-%) under
4001    scope=comm. see %-section_7_5-% for all wire mechanics.
4002
4003    management commands (client -> server):
4004      KVAL op=set scope=comm id=<gid> key=<key> :value
4005      KVAL op=del scope=comm id=<gid> key=<key>
4006      KVAL op=get scope=comm id=<gid> [key=<key>]
4007
4008    defined keys for scope=comm:
4009
4010      description -- wire markup (\n-encoded per %-section_2_4-%).
4011                     the community's public description, shown on
4012                     community info screens and invite previews.
4013                     links via [label](url) inline.
4014      icon        -- aid or https url (plain text).
4015      banner      -- aid or https url (plain text).
4016      color       -- 6-digit hex color, no '#' prefix (e.g. 'ff6600').
4017      rules       -- wire markup (\n-encoded per %-section_2_4-%).
4018                     the community's rules document.
4019
4020    key=name is not a valid scope=comm key. the community display name
4021    is owned exclusively by COMM op=update name= (%-section_8_0-%); there
4022    is exactly one source of truth. servers MUST reject
4023    KVAL op=set scope=comm key=name with ERR code=BADARG.
4024
4025    write permission: requires channel.manage or community.admin on the
4026    gid. servers MUST reject writes from uids without either permission
4027    with ERR code=NOPERM (per %-section_7_5b-% scope registry).
4028
4029    read permission: any member of the community. non-members of
4030    private communities receive ERR code=NOPERM.
4031
4032    broadcast: on a successful op=set or op=del the server broadcasts
4033    a KVAL op=info (or KVAL op=del) line to all members of the gid,
4034    per %-section_7_5c-% and %-section_7_5d-%. the acting uid is not carried in the KVAL
4035    broadcast; audit of who changed a key is reserved for the AUDIT
4036    extension (%-section_25-%). this is consistent with all other KVAL
4037    broadcasts: the verb carries state, not actor identity.
4038
4039    query reply format (mirrors %-section_7_5e-%):
4040      single-key:
4041        KVAL op=info scope=comm id=<gid> key=<key> ts=<unix-ms> :value
4042        OK verb=KVAL op=get scope=comm id=<gid> key=<key>
4043      all-keys:
4044        KVAL op=info scope=comm id=<gid> key=<key> ts=<unix-ms> :value
4045        ...
4046        OK verb=KVAL op=get scope=comm id=<gid> count=<n> total=<n>
4047
4048%- wire_markup = "Sect.  9" -%
4049========================================================================
4050%-wire_markup-% -- WIRE MARKUP
4051========================================================================
4052
4053  wire markup is a strict defined subset of CommonMark with extensions.
4054  all markup is OPTIONAL to render. servers relay body verbatim.
4055  a conforming L0 client may display raw text without any rendering.
4056
4057  9.1  decoding pipeline
4058
4059    clients MUST apply these steps in order:
4060
4061      1. receive raw payload (as relayed verbatim by server)
4062      2. unescape: left-to-right single pass per %-section_2_4-%
4063         resolve \n -> LF, \t -> TAB, \\ -> backslash
4064      3. apply wire markup rendering to the decoded string
4065
4066    markup is parsed on the decoded string, never on the wire form.
4067    servers never execute step 2 or step 3.
4068    this pipeline applies to all payloads in all verbs, not just MSG.
4069
4070    bot and non-rendering client pipeline:
4071      step 2 (unescape) is REQUIRED for ALL clients, including bots,
4072      CLI tools, and any client that does not render markup. a client
4073      that skips step 2 will present raw escape sequences to its user
4074      or application logic.
4075      step 3 (markup rendering) is OPTIONAL. a client MAY stop after
4076      step 2 and present the decoded plain text.
4077      clients MUST NOT apply step 2 more than once to a payload.
4078      the server relays the wire form verbatim; no intermediate
4079      re-encoding occurs, so a single unescape pass is always correct.
4080
4081  9.2  inline elements
4082
4083    *bold*             bold
4084    _italic_           italic
4085    ~strikethrough~    strikethrough
4086    `code`             inline code; monospace; no further parsing inside
4087    ||spoiler||        hidden until client action reveals it
4088    [text](url)        hyperlink
4089    :name:             standard unicode emoji by CLDR short name
4090    :gid/name:         custom community emoji (see %-section_10-%)
4091    @uid:name          user mention; uid is stable, name is display hint
4092    @rolid:name        role mention; rolid is stable, name is display hint
4093    @everyone          shorthand for @everyone:everyone (built-in role)
4094    @here              shorthand for @here:here (built-in role)
4095    #cid:name          channel mention
4096    ^mid               message reference / link
4097
4098    @everyone and @here are syntactic sugar. they expand to the standard
4099    @rolid:name form with the reserved literal rolids 'everyone' and
4100    'here'. clients that render them SHOULD look up their ROLE op=appearance
4101    data (color, emoji) from the role cache exactly as for any
4102    role mention.
4103
4104    for all @-mentions, clients use the id (before ':') to look up cached
4105    appearance and render accordingly. the :name portion is a display
4106    hint for L0/dumb clients only. clients MUST NOT use :name as an
4107    identity; always use the id.
4108
4109    if a message body contains @everyone or @here but rolmentions= does
4110    not include the corresponding built-in rolid, no PING is generated.
4111    the markup is rendered as plain highlighted text or ignored.
4112
4113  9.3  block elements
4114
4115    block elements depend on decoded LF characters (step 2 above).
4116    they are defined here for clients that implement block rendering.
4117
4118    ```lang\n...\n```   fenced code block; lang is optional hint
4119                        content between fences is literal; no markup
4120    > text              blockquote (single level; no nesting)
4121    # heading           h1
4122    ## heading          h2
4123    - item              unordered list item
4124    1. item             ordered list item
4125    ---                 horizontal rule (must be on its own decoded line)
4126
4127    context rules:
4128      board, forum:   full markup including headings
4129      text:           inline + blockquote + code block; headings ignored
4130      voice:          no markup context
4131
4132    clients MAY render blocks in any context; servers do not enforce.
4133
4134  9.4  emoji references
4135
4136    :name:       -- standard unicode emoji
4137    :gid/name:   -- custom community emoji
4138
4139    custom emoji resolution: see %-section_10-%.
4140
4141  9.5  attachments
4142
4143    ![alt](aid:<aid>)     inline image attachment
4144    [name](aid:<aid>)     inline file attachment
4145
4146    upload flow: see %-section_12-%.
4147
4148%- custom_emoji = "Sect.  10" -%
4149========================================================================
4150%-custom_emoji-% -- CUSTOM EMOJI MANAGEMENT
4151========================================================================
4152
4153  custom emoji are community-scoped named image assets.
4154  they are referenced in markup as :gid/name: (see %-section_9_4-%).
4155  emoji belong to a community (gid). there is no global emoji namespace.
4156
4157  10.1  declaring emoji
4158
4159    COMM op=emoji.add gid=<gid> name=<n> mime=<mimetype> [animated=0|1] aid=<aid>
4160
4161    name: [a-z0-9_], 1-32 chars, unique within the community.
4162    aid: must refer to a previously UPLOADED asset (%-section_12-%).
4163    mime: the MIME type of the asset (e.g. image/png, image/gif,
4164          image/webp). servers MUST validate mime= matches the asset
4165          recorded at UPLOAD time and reject mismatches with
4166          ERR code=BADARG. mime= is percent-encoded per %-section_2_3-%.
4167    animated: 1 if the asset contains animation (GIF/APNG/animated WebP);
4168              default 0. clients MAY derive this from mime= but animated=
4169              is provided as a convenience shortcut so clients that only
4170              need a boolean need not inspect MIME subtype details.
4171
4172    server replies:
4173    OK verb=COMM op=emoji.add gid=<gid> name=<n> eid=<eid>
4174
4175    eid is a server-assigned ULID. it is the stable reference.
4176    names may change; eids never do.
4177
4178    COMM op=emoji.rename gid=<gid> eid=<eid> name=<newname>
4179    COMM op=emoji.delete gid=<gid> eid=<eid>
4180
4181    server replies for rename and delete:
4182    OK verb=COMM op=emoji.rename gid=<gid> eid=<eid>
4183    OK verb=COMM op=emoji.delete gid=<gid> eid=<eid>
4184
4185    server broadcasts to all community members:
4186    EMOJI gid=<gid> eid=<eid> op=add|rename|delete name=<n>
4187          [url=<url>] [mime=<mimetype>] [animated=0|1]
4188
4189    op=add:    all fields present (url=, mime=, animated=)
4190    op=rename: url=, mime=, and animated= omitted; name carries new value
4191    op=delete: url=, mime=, and animated= omitted
4192
4193  10.2  listing community emoji
4194
4195    COMM op=emoji.list gid=<gid> [offset=<n>] [limit=<n>]
4196
4197    offset default: 0. limit default: 100. limit max: 500.
4198    servers MUST clamp limit to 500.
4199
4200    server replies with one EMOJI line per result, then OK:
4201    EMOJI gid=<gid> eid=<eid> op=info name=<n> url=<url> mime=<mimetype> animated=0|1
4202          ts=<unix-ms>
4203    EMOJI ...
4204    OK verb=COMM op=emoji.list gid=<gid> count=<n> total=<n>
4205
4206    count: number of lines in this page.
4207    total: total emoji in the community (for pagination).
4208
4209  10.3  resolving a single emoji
4210
4211    EMOJI op=get gid=<gid> name=<n>
4212    EMOJI op=get gid=<gid> eid=<eid>
4213
4214    server replies:
4215    EMOJI gid=<gid> eid=<eid> op=info name=<n> url=<url> mime=<mimetype> animated=0|1
4216          ts=<unix-ms>
4217    OK verb=EMOJI gid=<gid>
4218    or
4219    ERR code=NOTFOUND :emoji not found in community
4220
4221    EMOJI verb directionality:
4222      EMOJI is used in two directions. as a client query (%-section_10_3-%):
4223      the client sends EMOJI op=get with gid= and either name= or eid=,
4224      and the server replies with EMOJI op=info. as a server event
4225      (%-section_10_1-%): the server broadcasts EMOJI with op=add|rename|delete.
4226      as a server response to listing (%-section_10_2-%): the server sends
4227      EMOJI op=info lines. the op= tag unambiguously identifies the kind;
4228      parsers dispatch on verb then op=. a client-sent EMOJI line MUST
4229      carry op=get; servers MUST reject a client-sent EMOJI with any op=
4230      value other than get with ERR code=BADARG, and MUST reject a
4231      client-sent EMOJI with no op= with ERR code=BADARG.
4232      note: EMOJI previously used op= presence (client-sent = no op=;
4233      server-sent = op= always present) as its direction discriminant,
4234      making it the only verb in the protocol inconsistent with the uid=
4235      discriminant pattern. op=get on the client send form removes this
4236      asymmetry: all bidirectional verbs now use either uid= presence or
4237      a distinct op= value to distinguish direction. the client-sent
4238      op=get value is distinct from all server-sent op= values
4239      (info, add, rename, delete); parsers dispatch on op= value, not
4240      op= presence, with no special-case branch needed.
4241
4242    EMOJI query pipelining (normative):
4243      EMOJI op=get requests for DIFFERENT emoji within the same community
4244      (differing eid= or name=) MAY be pipelined; each EMOJI op=info reply
4245      line is self-identifying by eid=, so the client can route responses
4246      without pending-request disambiguation. the same-resource pipeline
4247      prohibition (%-section_2_5-%) applies at (gid, eid) or (gid, name) granularity,
4248      not at the gid level. clients MUST NOT pipeline two EMOJI op=get
4249      queries for the same (gid, eid) pair; those are same-resource and
4250      indistinguishable.
4251
4252  10.4  server push on subscribe
4253
4254    when a client SUBs to a channel within a community, the server MAY
4255    proactively send the full emoji list (same format as emoji.list)
4256    before the SUB OK. clients SHOULD cache per (gid, eid).
4257    cache is invalidated by EMOJI op=delete or op=rename broadcasts.
4258    cache entries SHOULD be treated as stale after 24h.
4259
4260    EMOJI pre-SUB-OK push routing (normative):
4261      EMOJI op=info lines arriving before a pending SUB OK are cache-warm
4262      push lines. parsers MUST route them by gid= to the emoji cache for
4263      that community. the pending SUB request provides the frame context;
4264      %-section_2_5-% pending-request state covers these lines as part of the SUB
4265      response. this is the same rule as the ROLE op=info pre-SUB-OK push
4266      (%-section_8_2a-%); both are governed by the cache-warm push principle
4267      defined there.
4268
4269  10.5  upload flow for emoji assets
4270
4271    UPLOAD gid=<gid> name=<urlencoded-name> size=<bytes>
4272           mime=<mimetype> purpose=emoji
4273
4274    purpose=emoji applies emoji-specific size/dimension limits.
4275    typical: 256KB / 128x128px static; 512KB animated.
4276    server replies with standard UPLOAD OK (%-section_12-%).
4277    after UPLOADED confirmation the aid is valid for emoji.add.
4278
4279  10.6  cross-community emoji (cap: xemoji)
4280
4281    servers advertising cap=xemoji allow :origin-gid/name: references
4282    from any community the user belongs to. resolution is unchanged;
4283    server proxies the lookup if uid is a member of origin-gid.
4284    servers without xemoji return ERR code=NOPERM on cross-gid lookups.
4285
4286  10.7  permissions
4287
4288    emoji.add, emoji.rename, emoji.delete require:
4289      community.admin OR emoji.manage on any assigned role
4290
4291    all members may use emoji in message bodies by default (governed by
4292    msg.send). to restrict emoji use in reactions specifically, deny
4293    react.use on the relevant role or channel. emoji in markup
4294    (:name: references in MSG bodies) are part of the message content
4295    and are not separately gateable beyond msg.send.
4296
4297%- boards_forums_threads = "Sect.  11" -%
4298========================================================================
4299%-boards_forums_threads-% -- BOARDS, FORUMS, AND THREADS
4300========================================================================
4301
4302  boards are channel type=board. forums are channel type=forum.
4303  both use MSG as the transport verb; title= and tags= tags carry
4304  structured metadata. text channels (type=text) also support threads
4305  via tid=. there is no POST verb; THREAD is the lifecycle management
4306  verb for rename, lock, unlock, and info operations on threads.
4307
4308  11.1  board posts
4309
4310    a board post is a MSG with title= present on a type=board channel.
4311    tags= is optional and used for filtering (see LIST, %-section_11_4-%).
4312    the body carries the full post content and MAY be empty (title= alone
4313    is a valid board post, e.g. a link post where the title is the link).
4314
4315    servers MUST reject MSG without title= on type=board channels with
4316    ERR code=BADARG :title required on board channels.
4317    servers MUST reject MSG with tid= on type=board channels with
4318    ERR code=BADARG :threads not supported on board channels.
4319    servers MUST reject MSG by a uid without board.post permission with
4320    ERR code=NOPERM.
4321
4322    board posts may be edited and deleted normally (%-section_6_4-%, 6.5).
4323    editing a board post may update both body and tags=; title= is fixed
4324    after creation and MUST NOT be changed via EDIT. servers MUST ignore
4325    title= in EDIT payloads.
4326
4327  11.2  forum threads
4328
4329    a forum thread is started by sending MSG with title= and without
4330    tid= on a type=forum channel. the server assigns a new tid= and
4331    includes it in both the OK and the broadcast. all subsequent replies
4332    to the thread are MSG with tid=<assigned-tid>. replies MUST NOT
4333    include title=.
4334
4335    servers MUST reject MSG with both tid= and title= present with
4336    ERR code=BADARG :title not permitted on thread replies.
4337    servers MUST reject MSG without title= and without tid= on type=forum
4338    channels with ERR code=BADARG :title required to start a forum thread.
4339
4340    fetching thread replies:
4341      HIST cid=<cid> tid=<tid> [before=<mid>] [after=<mid>] [limit=<n>]
4342      returns all messages in the thread in mid order (%-section_6_6-%).
4343      the opening message (which carries title=) is included.
4344
4345  11.2a  thread lifecycle (forum and text channels)
4346
4347    threads on both forum and text channels support a small set of
4348    management operations via the THREAD verb.
4349
4350    renaming a thread (forum channels only):
4351
4352      THREAD op=rename cid=<cid> tid=<tid> :new title
4353
4354      text-channel threads have no title (%-section_11_3-%); servers MUST
4355      reject THREAD op=rename on type=text channels with ERR code=BADARG.
4356
4357      only the thread's original author (the uid whose MSG created the
4358      thread) or any member with thread.manage permission may rename.
4359      servers MUST reject otherwise with ERR code=NOPERM.
4360      title payload: encoded per %-section_2_4-%; max 256 chars decoded.
4361
4362      server replies:
4363      OK verb=THREAD op=rename cid=<cid> tid=<tid>
4364
4365      server broadcasts to all cid subscribers:
4366      THREAD op=rename cid=<cid> tid=<tid> uid=<uid> ts=<unix-ms>
4367             :new title
4368
4369    locking a thread (forum and text channels):
4370
4371      THREAD op=lock cid=<cid> tid=<tid>
4372
4373      a locked thread accepts no new replies. existing messages are
4374      unaffected. only the thread's author (the uid stored as author= in
4375      the thread record; see THREAD op=info) or a member with thread.manage
4376      may lock. servers MUST reject new MSG tid= on a locked thread with
4377      ERR code=FORBIDDEN :thread is locked.
4378
4379      server stores the locking uid internally as locked_by=<uid>.
4380
4381      server replies:
4382      OK verb=THREAD op=lock cid=<cid> tid=<tid>
4383
4384      server broadcasts to all cid subscribers:
4385      THREAD op=lock cid=<cid> tid=<tid> uid=<uid> ts=<unix-ms>
4386
4387    unlocking a thread:
4388
4389      THREAD op=unlock cid=<cid> tid=<tid>
4390
4391      unlock permission rules:
4392        - if locked_by == requesting uid: allowed unconditionally.
4393          an author who locked his own thread may reopen it freely.
4394        - if locked_by != requesting uid (i.e. a moderator locked it):
4395          thread.manage is required. the original author (author= in the
4396          thread record) CANNOT override a moderator lock without
4397          thread.manage.
4398        - community.admin holders may always unlock (per %-section_8_2-%).
4399
4400      this rule has no trap: an author who self-locks can always
4401      self-unlock. a moderator lock is protected from author override.
4402
4403      server replies:
4404      OK verb=THREAD op=unlock cid=<cid> tid=<tid>
4405
4406      server broadcasts to all cid subscribers:
4407      THREAD op=unlock cid=<cid> tid=<tid> uid=<uid> ts=<unix-ms>
4408
4409    THREAD verb directionality (normative):
4410      THREAD is used in two roles: as a client-initiated lifecycle command
4411      (op=rename, op=lock, op=unlock) and as a query (op=info).
4412
4413      for op=info: THREAD op=info is always a query-reply verb. it is
4414      never sent as a live event. parsers MUST route THREAD op=info lines
4415      via %-section_2_5-% pending-request state (the pending THREAD op=info request).
4416      THREAD op=info MUST NOT appear as a server-initiated unsolicited line.
4417
4418      for op=rename, op=lock, op=unlock: these are bidirectional. the
4419      client sends the command (no uid=, no ts=); the server broadcasts
4420      the event (carries uid= and ts=). parsers MUST use uid= presence
4421      to distinguish direction: a client-sent THREAD op=rename/lock/unlock
4422      line carries cid= and tid= only (and a payload for rename); a server-
4423      broadcast line carries uid= and ts= in addition. servers MUST NOT
4424      broadcast THREAD op=rename/lock/unlock without uid=; clients MUST
4425      NOT send these with uid=. this is the same normative rule used by
4426      EDIT (%-section_6_4-%), DEL (%-section_6_5-%), and PIN (%-section_11_4-%).
4427
4428    querying thread metadata:
4429
4430      THREAD op=info cid=<cid> tid=<tid>
4431
4432      server replies:
4433      THREAD op=info cid=<cid> tid=<tid> opener=<mid> author=<uid>
4434             locked=0|1 reply_count=<n> ts=<unix-ms> [:title]
4435      OK verb=THREAD op=info cid=<cid> tid=<tid>
4436      or
4437      ERR code=NOTFOUND :thread not found
4438
4439      opener=  mid of the message that created the thread.
4440               for forum threads: the mid of the title-bearing MSG that
4441               started the thread (server-assigned; equals the tid).
4442               for text-channel threads: the mid of the root message
4443               (the message whose mid equals the tid). the server
4444               determines this at implicit thread creation time (first
4445               MSG with that tid= accepted) and stores it permanently.
4446      author=  uid of the thread's original author. this is the uid of
4447               the message at opener=. servers MUST store author= on the
4448               thread record at implicit creation time so that lock and
4449               unlock permission checks (%-section_11_2a-%) do not require
4450               a message lookup at auth-check time.
4451      locked=  current lock state.
4452      reply_count= number of live (non-deleted) replies, excluding the
4453               opening message. DEL tombstones are NOT counted; only
4454               replies for which no DEL tombstone exists contribute.
4455               clients querying HIST cid=<cid> tid=<tid> will observe a
4456               count= that includes tombstone lines (DEL events) and may
4457               therefore exceed reply_count=. this divergence is
4458               intentional: reply_count= is a display counter for user-
4459               facing "N replies" indicators; HIST count= is a raw event
4460               count for pagination arithmetic. they measure different
4461               things and must not be conflated.
4462      ts=      the last-modified time of the thread entity: the unix-ms
4463               of the most recent THREAD op=rename, op=lock, or op=unlock
4464               event on this thread; equals the thread creation time (the
4465               ts= of the opening MSG) if no such event has occurred.
4466               this is the "last-modified time of the entity" semantic
4467               used by every op=info ts= in the protocol. it is NOT the
4468               time at which this response line was generated. parsers
4469               MUST NOT treat this ts= as a reply-generation timestamp.
4470               this is a normative definition; implementors MUST store
4471               and update this value on every thread lifecycle event.
4472      title    current title (payload); absent for text-channel threads
4473               (which have no title).
4474
4475    THREAD events in HIST:
4476      THREAD op=rename, op=lock, op=unlock events ARE included in HIST
4477      responses for the channel (not filtered by tid=). clients replay
4478      them to reconstruct current thread state. THREAD op=info is a
4479      query-reply only and does not appear in HIST.
4480
4481  11.3  threads on text channels
4482
4483    type=text channels support lightweight threading via tid=. a thread
4484    is implicitly created the first time a MSG with a new tid= value is
4485    accepted on the channel. thread.create permission rules are defined
4486    in %-section_8_2-%; see that section for the distinction between text
4487    and forum channel handling.
4488
4489    at implicit thread creation time, the server MUST store on the thread
4490    record:
4491      - tid= (equals the mid of the root message)
4492      - author= (the uid of the MSG that triggered creation; used for
4493        lock/unlock permission checks without a message lookup)
4494      - opener= (same as tid= for text-channel threads: the mid of the
4495        root message is the thread's opening message)
4496
4497    root message annotation (normative):
4498      when the server implicitly creates a text-channel thread, it MUST
4499      retroactively annotate the stored root message record with
4500      tid=<tid>. this ensures that subsequent HIST cid=<cid> tid=<tid>
4501      queries return the root message alongside its replies, giving
4502      clients a complete thread view from a single HIST request.
4503
4504      the original live broadcast of the root message (which carried no
4505      tid= because the thread did not yet exist) is NOT re-broadcast.
4506      clients that received the root message live will see it without
4507      tid=; they learn the root is the thread opener because its mid
4508      equals the tid= of the first threaded reply they receive.
4509
4510      consequently, MSG lines returned by HIST may carry tid= even if
4511      the original live broadcast of that message did not. clients MUST
4512      handle this. the live-vs-HIST divergence is intentional: the
4513      alternative (requiring clients to separately fetch the root on
4514      every HIST tid= query) produces worse client ergonomics for no
4515      protocol gain. see %-section_24-% rationale for further discussion.
4516
4517    text-channel threads do NOT have titles. THREAD op=rename is
4518    not applicable and is rejected (see %-section_11_2a-% for the normative rule).
4519    THREAD op=lock and op=unlock are valid on text-channel threads.
4520
4521    clients typically display text-channel threads as an expandable
4522    reply chain below the root message. the root message's mid IS its
4523    tid if the client created the thread by setting tid=<mid-of-root>;
4524    servers MUST accept this pattern and treat it as starting a new
4525    thread.
4526
4527    servers MUST reject MSG with tid= on a text channel if the uid
4528    lacks thread.create permission and the tid does not already exist
4529    (i.e. no prior MSG with that tid= has been accepted on this cid).
4530
4531    cross-channel tid= injection rejection (normative):
4532      servers MUST reject MSG cid=<cid> tid=<X> on a text channel if X
4533      is not an existing mid in this cid, regardless of the sender's
4534      permissions. the rejection response is:
4535        ERR code=NOTFOUND :thread root not found in this channel
4536      this rule applies even when the uid has thread.create permission.
4537      the purpose is to prevent a client from implicitly creating a thread
4538      rooted at a mid from a different channel or from a non-existent mid,
4539      which would produce a thread record with an unresolvable opener=.
4540      when tid=<X> refers to the sender's own just-sent message in this
4541      cid, the root mid is already stored before the reply MSG arrives;
4542      no exception is required.
4543
4544  11.4  pins
4545
4546    PIN mid=<mid>
4547    UNPIN mid=<mid>
4548
4549    requires msg.pin permission (or board.manage on board channels).
4550    server broadcasts:
4551    PIN   mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
4552    UNPIN mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
4553
4554    server replies to the originating client:
4555    OK verb=PIN mid=<mid> cid=<cid>
4556    OK verb=UNPIN mid=<mid> cid=<cid>
4557
4558    cid= is echoed in the OK so the originating client can route the
4559    confirmation without pending-request state when pipelining PIN/UNPIN
4560    across channels. the server derives cid from its mid->cid index at
4561    validation time, consistent with EDIT, DEL, REACT, and UNREACT (%-section_2_5-%).
4562
4563    idempotency (normative):
4564      PIN and UNPIN are idempotent. servers MUST accept PIN on an
4565      already-pinned message and return OK verb=PIN mid=<mid> silently
4566      without re-broadcasting the PIN event. servers MUST accept UNPIN
4567      on a message that is not pinned and return OK verb=UNPIN mid=<mid>
4568      silently without broadcasting. this is consistent with PING
4569      op=dismiss and COMM op=member.untimeout idempotency.
4570
4571    PIN/UNPIN verb directionality (normative):
4572    client-sent forms: PIN mid=<mid> and UNPIN mid=<mid> (operate on a
4573    message; carry mid= only); PIN op=list cid=<cid> (query; carries
4574    cid= only, no mid= tag). server-broadcast: PIN/UNPIN carrying uid=,
4575    cid=, and ts= in addition to mid= (no op= tag). server query
4576    response: PIN op=info lines (op= present; never a live broadcast).
4577    parsers MUST use uid= to distinguish client-sent bare PIN/UNPIN (no
4578    uid=) from server-broadcast (uid= present). parsers MUST use op= to
4579    distinguish PIN op=list and PIN op=info from bare PIN/UNPIN.
4580    servers MUST NOT send bare PIN/UNPIN with no uid=; clients MUST NOT
4581    send PIN/UNPIN with uid=. this is the same normative rule used by
4582    EDIT (%-section_6_4-%), DEL (%-section_6_5-%), and KVAL (%-section_7_5-%).
4583
4584    querying the current pinned message set:
4585
4586      PIN op=list cid=<cid> [offset=<n>] [limit=<n>]
4587
4588      returns the current set of pinned messages in a channel. any member
4589      subscribed to the channel may call this. requires community membership
4590      for community channels; requires DM participation for DM channels.
4591
4592      offset default: 0. limit default: 50. limit max: 200.
4593      servers MUST clamp limit to 200.
4594
4595      server replies with one PIN op=info line per pinned message, in
4596      pin-time order (oldest pin first), then OK:
4597
4598        PIN op=info mid=<mid> cid=<cid> uid=<uid> ts=<unix-ms>
4599        ...
4600        OK verb=PIN op=list cid=<cid> count=<n> total=<n>
4601
4602      uid=  the uid of the member who pinned the message.
4603      ts=   the unix-ms at which the message was pinned.
4604      count: number of lines in this page. total: total pinned messages
4605      in this channel.
4606
4607      the pin index is the server-side store that makes PIN idempotency
4608      (no re-broadcast on double-PIN) possible; PIN op=list surfaces it
4609      as a query. clients MUST NOT reconstruct pin state solely by
4610      replaying HIST; they MUST use PIN op=list on channel join to warm
4611      their pin display state.
4612
4613      PIN op=info is listed in %-section_2_5-% as a routable response verb. parsers
4614      route PIN op=info lines via pending-request state (the outstanding
4615      PIN op=list request). PIN op=info is never sent as an unsolicited
4616      live broadcast; live pin events use the bare PIN broadcast form
4617      (uid= present, no op= tag).
4618
4619  11.5  listing
4620
4621    LIST cid=<cid> [before=<mid>] [after=<mid>] [limit=<n>]
4622         [tags=<csv>] [locked=0|1]
4623
4624    returns top-level content only:
4625      board:   all posts (MSG with title=). tags= filters to posts
4626               that contain ALL listed tags (intersection). locked= is
4627               not applicable to board and MUST be ignored.
4628      forum:   thread-opening messages only (no replies). tags= is
4629               not applicable to forums and MUST be ignored. locked=
4630               filters to locked (1) or unlocked (0) threads.
4631               if a thread's opening message has been deleted, the server
4632               MUST include a DEL tombstone line (DEL mid=<opener-mid>
4633               uid=<uid> ts=<unix-ms>) in place of the MSG line so
4634               clients can render "original post deleted" consistently.
4635               THREAD op=info remains queryable for the thread regardless
4636               of opener deletion.
4637      text:    all top-level messages: those whose mid does not appear
4638               as a reply (tid=) under any other message. the server
4639               determines this from its thread index, not from the
4640               stored tid= field on the message itself (a message that
4641               was later referenced as a thread root by a reply is
4642               treated as top-level). tags= is not applicable to text
4643               channels and MUST be ignored. locked= filters to locked
4644               (1) or unlocked (0) threads, consulting the same per-tid
4645               lock state stored for THREAD op=lock/unlock (%-section_11_2a-%); servers MUST maintain a thread lock index for
4646               text-channel threads to satisfy this query.
4647
4648    before= and after= are mutually exclusive; servers MUST reject
4649    a request containing both with ERR code=BADARG. default behavior
4650    (no before= or after=) returns the newest limit= entries in
4651    ascending mid order.
4652
4653    limit default: 20. limit max: 100. servers MUST clamp to 100.
4654    server replies with MSG lines in mid order, then:
4655    OK verb=LIST cid=<cid> count=<n> has_more=0|1
4656
4657%- attachments = "Sect.  12" -%
4658========================================================================
4659%-attachments-% -- ATTACHMENTS
4660========================================================================
4661
4662  12.1  upload negotiation
4663
4664    UPLOAD [cid=<cid>] [gid=<gid>] name=<urlencoded-name>
4665           size=<bytes> mime=<mimetype> [purpose=<purpose>]
4666
4667    purpose values:
4668      message  (default) -- a file or image to be attached to a MSG.
4669                            cid= is required. gid= is ignored.
4670      emoji              -- an emoji asset to be registered with a
4671                            community. gid= is required. cid= is ignored.
4672                            emoji-specific size/dimension limits apply
4673                            (see %-section_10_5-%).
4674      avatar             -- a user avatar image to be used with IDENTIFY
4675                            avatar= (%-section_4_5-%). neither cid= nor gid=
4676                            is required or used; both are ignored.
4677                            servers SHOULD enforce avatar-specific size
4678                            and dimension limits (RECOMMENDED: 256KB /
4679                            512x512px maximum; square aspect preferred).
4680
4681    if purpose= is absent, message is assumed and cid= is required.
4682    servers MUST reject an UPLOAD with purpose=message and no cid= with
4683    ERR code=BADARG. servers MUST reject an UPLOAD with purpose=emoji
4684    and no gid= with ERR code=BADARG.
4685    servers MUST reject an UPLOAD with an unrecognized purpose= value
4686    with ERR code=BADARG :unknown upload purpose. unknown values cannot
4687    be silently ignored because purpose= drives server-side validation
4688    policy (size limits, required context tags); forward-compatible
4689    clients MUST negotiate a known purpose or use a capability check
4690    before sending a non-standard purpose= value.
4691
4692    server replies:
4693    OK verb=UPLOAD aid=<aid> name=<urlencoded-name> size=<bytes>
4694       mime=<mimetype> url=<upload-url> method=PUT
4695       [headers=<urlencoded-json>]
4696    or
4697    ERR code=TOOBIG :max size is N bytes
4698
4699    method=PUT is the only defined upload method in 0.8. servers MUST
4700    send method=PUT; clients MUST use HTTP PUT to the url= provided.
4701    future revisions MAY define additional method values; clients MUST
4702    treat an unrecognised method= value as ERR code=UNKNOWN and
4703    re-negotiate rather than attempting an unsupported HTTP method.
4704
4705    client uploads via HTTP PUT to the returned url.
4706    after the HTTP upload completes, the server sends asynchronously:
4707    UPLOADED aid=<aid>
4708
4709    if the HTTP upload fails, no UPLOADED is sent.
4710    client may retry by restarting from UPLOAD negotiation.
4711
4712    UPLOAD op=status aid=<aid>
4713
4714      polls the upload state for a previously negotiated aid. this is
4715      the synchronous alternative to waiting for the UPLOADED event;
4716      it is the correct path for CLI/TUI/ACME clients that cannot
4717      maintain a parallel HTTP connection and WIRE connection cleanly.
4718
4719      any authenticated uid may query an aid it owns.
4720      servers MUST return ERR code=NOTFOUND for unknown or expired aids.
4721
4722      server replies:
4723      UPLOAD op=status aid=<aid> purpose=<purpose> state=pending|complete|failed
4724             [cid=<cid>] [gid=<gid>] [url=<upload-url>] [expires=<unix-ms>]
4725      OK verb=UPLOAD op=status aid=<aid>
4726
4727      purpose= echoes the purpose tag from the original UPLOAD negotiation.
4728      cid= is echoed when purpose=message; gid= is echoed when
4729      purpose=emoji; when purpose=avatar, neither cid= nor gid= is
4730      present -- avatar assets are uid-scoped and carry no channel or
4731      community context. clients managing multiple in-flight uploads
4732      across different channels, communities, or purposes MUST use
4733      these tags to route status replies without tracking out-of-band
4734      state.
4735
4736      state=pending:  HTTP PUT not yet received by server.
4737      state=complete: UPLOADED; aid is valid for use in MSG and emoji.add.
4738                      an aid in state=complete MUST remain valid for at
4739                      least 300 seconds after the state transition, giving
4740                      the client a guaranteed window to compose and send
4741                      the MSG. once an aid is referenced in an accepted
4742                      MSG (the MSG OK is returned), the aid is permanently
4743                      referenced and servers MUST NOT expire it. unreferenced
4744                      aids (state=complete but never used in a MSG) MAY be
4745                      expired by the server after a server-defined TTL
4746                      (RECOMMENDED: 24 hours minimum). a client that receives
4747                      WARN code=ATTACH_STRIPPED on a MSG it believed had a
4748                      valid complete aid MUST re-upload and retry.
4749
4750                      aid permanence and message deletion (normative):
4751                      if the MSG that references an aid is subsequently
4752                      deleted (DEL), the aid's permanence is preserved only
4753                      as long as at least one other non-deleted MSG on the
4754                      server also references it. if deletion of a MSG removes
4755                      the last reference to an aid, the server MAY expire
4756                      that aid at its discretion; the permanence guarantee
4757                      no longer applies. clients MUST NOT assume an aid
4758                      remains resolvable after all referencing messages have
4759                      been deleted.
4760
4761                      there is no protocol operation to update or remove the
4762                      attachments= structural tag on a sent message. EDIT
4763                      replaces body only (%-section_6_4-%); attachments= is
4764                      immutable after acceptance. a user who wishes to
4765                      retract an attachment must delete the message (DEL)
4766                      and re-send without it. clients that receive
4767                      ERR code=NOTFOUND from ATTACH on a referenced aid
4768                      SHOULD display an "attachment unavailable" placeholder.
4769      state=failed:   HTTP PUT failed or was not received within the
4770                      server-defined upload window (RECOMMENDED: 300s).
4771                      the aid is invalid; client must re-negotiate.
4772
4773      note for CLI clients: url= in state=pending is the HTTP PUT upload
4774      URL, not a download URL. after confirming state=complete, a client
4775      needing to verify the download URL must issue ATTACH aid=<aid>
4776      (%-section_12_3-%). for the typical CLI flow (upload then attach to MSG),
4777      no download URL is needed: the aid is referenced in the MSG body and
4778      recipients resolve it via their own ATTACH call.
4779
4780      url= is present only in state=pending and carries the same upload
4781      URL from the original UPLOAD OK, allowing a client that lost the
4782      URL to re-obtain it without full re-negotiation. the upload URL
4783      is valid until the unix-ms deadline carried in expires=; clients
4784      MUST NOT attempt the HTTP PUT after that deadline.
4785      expires= is present only in state=pending and carries the unix-ms
4786      deadline after which the upload slot will be marked failed.
4787
4788      servers MUST keep aid status queryable for at least 60 seconds
4789      after state transitions to complete or failed.
4790
4791  12.2  referencing in messages
4792
4793    after UPLOADED, reference in markup:
4794    ![alt](aid:<aid>)     image
4795    [name](aid:<aid>)     file
4796
4797  12.3  download resolution
4798
4799    ATTACH aid=<aid>
4800    server replies:
4801    ATTACH aid=<aid> name=<urlencoded-name> size=<bytes> mime=<mimetype>
4802           url=<download-url>
4803    OK verb=ATTACH aid=<aid>
4804    or
4805    ERR code=NOTFOUND :unknown aid
4806
4807    download URLs are time-limited. clients MUST NOT persist them.
4808
4809    ATTACH follows the same dual-direction pattern as EMOJI (%-section_10_3-%):
4810    client sends ATTACH with aid= only; server replies with ATTACH carrying
4811    the full metadata. parsers MUST use tag presence to distinguish direction:
4812    a client-sent ATTACH line carries only aid=; a server reply carries
4813    name=, size=, mime=, and url= in addition to aid=. this is the same
4814    normative rule used by EMOJI (%-section_10_3-%) and KVAL (%-section_7_5-%).
4815
4816%- reactions = "Sect.  13" -%
4817========================================================================
4818%-reactions-% -- REACTIONS
4819========================================================================
4820
4821  REACT   mid=<mid> eid=<eid|cldr-name>
4822  UNREACT mid=<mid> eid=<eid|cldr-name>
4823
4824  for standard unicode emoji: eid is the CLDR short name (e.g. thumbsup)
4825  for custom emoji: eid is the server-assigned ULID.
4826
4827  server replies to the originating client:
4828  OK verb=REACT   mid=<mid> cid=<cid> eid=<eid> ts=<unix-ms>
4829  OK verb=UNREACT mid=<mid> cid=<cid> eid=<eid> ts=<unix-ms>
4830
4831  server broadcasts:
4832  REACT   mid=<mid> cid=<cid> uid=<uid> eid=<eid> name=<n> ts=<unix-ms>
4833  UNREACT mid=<mid> cid=<cid> uid=<uid> eid=<eid> ts=<unix-ms>
4834
4835  cid= in both OK and broadcast forms makes every REACT and UNREACT line
4836  self-identifying by channel, consistent with PIN, UNPIN, DEL, and EDIT
4837  (%-section_2_5-%). the server derives cid from its mid->cid index at validation time;
4838  no additional lookup is required. cid= in the OK allows the originating
4839  client to route the confirmation without pending-request state when
4840  pipelining REACTs across channels.
4841  the client-sent forms carry only mid= and eid=; both cid= and uid= are
4842  absent. parsers MUST use uid= presence as the primary direction
4843  discriminant: a client-sent REACT or UNREACT line carries only mid=
4844  and eid= (no uid=, no cid=, no ts=); a server-sent line (broadcast or
4845  OK) carries cid= in addition, and a broadcast carries uid= and ts= as
4846  well. servers MUST reject a client-sent REACT or UNREACT that carries
4847  cid= or uid= with ERR code=BADARG.
4848
4849  name=: CLDR name or custom emoji name; carried on REACT broadcast for
4850  display convenience so receivers can render the emoji name without a
4851  cache lookup. UNREACT broadcast intentionally omits name=: a client
4852  always receives the corresponding REACT before it can receive an UNREACT
4853  for the same (mid, eid, uid) pair, so the name is already in the client's
4854  reaction cache. in a truncated HIST window where the prior REACT is not
4855  present, the UNREACT is a floor-at-zero no-op on the reaction count and
4856  name= is irrelevant. this asymmetry is intentional and permanent.
4857
4858  mid existence rules (normative):
4859    servers MUST return ERR code=NOTFOUND if the mid is entirely unknown
4860    (no live record and no DEL tombstone on this server).
4861    servers MUST return ERR code=GONE if mid has a DEL tombstone
4862    (message was deleted). existing REACT/UNREACT events recorded before
4863    the deletion remain in HIST and are replayed correctly; clients see
4864    the DEL tombstone and floor their reaction counts accordingly.
4865
4866  clients build reaction counts from the event stream.
4867  servers MUST NOT aggregate or deduplicate reaction events.
4868  servers MUST include REACT and UNREACT events in HIST (%-section_6_6-%).
4869  clients replay HIST in order to reconstruct reaction state:
4870    for each (mid, eid, uid) tuple:
4871      REACT adds one. UNREACT removes one. floor at zero.
4872
4873%- voice_video = "Sect.  14" -%
4874========================================================================
4875%-voice_video-% -- VOICE AND VIDEO (OPTIONAL EXTENSION, cap: voice)
4876========================================================================
4877
4878  voice and video are optional. clients that do not implement them MUST
4879  ignore all VOICE, VSTREAM, and VFRAME lines. servers that do not
4880  implement them return ERR UNKNOWN on VOICE and VSTREAM verbs.
4881
4882  14.0  overview
4883
4884    design: orchestration-only relay + direct peer-to-peer.
4885
4886    the main WIRE connection carries voice and video control: joining,
4887    leaving, participant status, key exchange. the server broadcasts peer
4888    addresses and authentication tokens.
4889
4890    audio and video frames travel directly between clients on separate
4891    binary-framed connections (not WIRE protocol). clients establish direct
4892    TCP/TLS connections, authenticate via tokens, and exchange encrypted
4893    frames (if enc=1). the server does not relay frame bytes.
4894
4895    this model prioritizes:
4896      - simplicity: no relay bandwidth cost, direct paths, one way to do each thing.
4897      - orthogonality: orchestration (WIRE) separate from frames (binary).
4898      - efficiency: zero base64 overhead, no server frame processing.
4899      - encryption: same Sender Key mechanism as MSG (Sect. %-section_29_2b-%).
4900
4901  14.1  orchestration: joining a call
4902
4903    sent on the main WIRE connection:
4904
4905      VOICE op=join cid=<cid> [codec=<audio-codec>] [streams=<csv>]
4906
4907    tags:
4908      codec=<opus|pcm16le>: client's preferred audio codec (optional).
4909      streams=<csv>: comma-separated list of streams to send (optional).
4910                     default: camera. examples: camera, screen:0, screen:0,screen:1.
4911
4912    permission check (normative):
4913      servers MUST verify that the requesting uid is a member of the
4914      community that owns the voice channel. non-members MUST receive
4915      ERR code=NOPERM.
4916
4917
4918  14.1a  orchestration: leaving a call
4919
4920    sent on the main WIRE connection:
4921
4922      VOICE op=leave cid=<cid>
4923
4924    server broadcasts to remaining participants:
4925
4926      VOICE op=leave cid=<cid> uid=<departing-uid> ts=<unix-ms>
4927
4928    when the count of participants in a channel reaches 0, the server
4929    clears the audio codec selection (codec reset). on the next VOICE
4930    op=join for that cid, a new codec is negotiated.
4931
4932    if a participant's WIRE connection drops, the server MUST treat
4933    it as an implicit VOICE op=leave for all cids that uid was
4934    participating in.
4935
4936    server replies to the requesting client:
4937
4938      OK verb=VOICE op=join cid=<cid> codec=<selected-codec>
4939         vaddr=<host:port> vsid=<token> [pubkey=<b64url>]
4940         [media_codecs=<csv>]
4941
4942    tags:
4943      codec=<opus|pcm16le>: audio codec selected for this channel session.
4944                            all participants use the same audio codec.
4945      vaddr=<host:port>:    address for direct P2P connection. in this spec, vaddr
4946                            points to the server's voice endpoint. clients behind NAT may not
4947                            be able to establish direct connections; NAT traversal (STUN, TURN,
4948                            hole-punching) is implementation-defined and not required by this spec.
4949      vsid=<token>:         authentication token (32 bytes, opaque).
4950                            valid for 30s, scoped to (uid, cid).
4951      pubkey=<b64url>:      client's Ed25519 public key (for X3DH key derivation).
4952      media_codecs=<csv>:   server-supported video codecs (e.g., vp8,h264).
4953
4954    server also broadcasts to existing participants on the main WIRE connection:
4955
4956      VOICE op=join cid=<cid> uid=<new-uid> vaddr=<new-addr:port>
4957            vsid=<new-token> pubkey=<new-pubkey-b64url>
4958            [streams=<csv>] ts=<unix-ms>
4959
4960    existing participants use this to:
4961      1. learn the new participant's address and pubkey.
4962      2. establish direct P2P connection to the new participant.
4963      3. derive shared encryption keys (if enc=1 channel).
4964
4965  14.1b  audio activity indicator
4966
4967    sent on the main WIRE connection (fire-and-forget):
4968
4969      VOICE op=talk cid=<cid> active=<0|1>
4970
4971    tags:
4972      active=1: uid is currently speaking (has audio activity).
4973      active=0: uid is no longer speaking (activity ended or muted).
4974
4975    server broadcasts to all participants with subscriptions in the cid:
4976
4977      VOICE op=talk cid=<cid> uid=<uid> active=<0|1> ts=<unix-ms>
4978
4979    clients use this to update speaker indicators in the UI. the
4980    activity indicator is ephemeral and not persisted; old VOICE op=talk
4981    events are not stored in HIST.
4982
4983
4984  14.2  direct peer-to-peer connection
4985
4986    after receiving VOICE op=join OK and peer broadcasts, the client
4987    establishes a direct TCP/TLS connection to each peer's vaddr.
4988
4989    connection handshake:
4990
4991      CLIENT sends: [token: 32 bytes] [enc_flag: 1 byte]
4992                    (token is vsid from VOICE op=join broadcast)
4993                    (enc_flag: 0x00 = unencrypted, 0x01 = encrypted)
4994
4995      SERVER (peer) validates token, verifies (uid, cid) match.
4996      PEER sends:   ACK [0x00]
4997
4998      If enc_flag=0x01 (encrypted channel):
4999        CLIENT sends: [key_version: 4 bytes (big-endian)]
5000        PEER:         derives Sender Key, reads key_version.
5001                      frame exchange begins with enc_flag=0x01.
5002
5003    frame format (binary, no WIRE protocol, no base64):
5004
5005      audio frame (VFRAME):
5006        [0x01] [flags: 1B] [key_epoch: 1B] [seq: 4B] [length: 2B] [payload: N bytes]
5007
5008        flags:  bit 0 = encrypted (0=no, 1=yes)
5009                bits 1-7 = reserved
5010
5011        key_epoch: tracks the current Sender Key generation (mod 256).
5012                   used for key rotation detection. incremented on each
5013                   PUBKEY op=keysync that rotates the key. receivers that
5014                   receive frames with a stale key_epoch should try both
5015                   the current key and the previous key (in case of in-flight
5016                   rotation). if neither decrypts, frame is silently dropped.
5017
5018        seq:    sequence number (big-endian), starts at 0 per sender.
5019                used for loss detection and ordering.
5020
5021        length: payload size in bytes (big-endian).
5022
5023      video frame (VSTREAM):
5024        [0x02] [flags: 1B] [key_epoch: 1B] [sid_len: 1B] [stream_id: M bytes]
5025        [codec: 1B] [seq: 4B] [length: 2B] [payload: N bytes]
5026
5027        flags:  bit 0 = encrypted
5028                bit 1 = keyframe (0=delta, 1=keyframe)
5029                bits 2-7 = reserved
5030
5031        stream_id: ASCII string (e.g., "camera", "screen:0", "screen:1").
5032                   length given by sid_len (1-16 bytes expected).
5033
5034        codec:  0x01 = VP8, 0x02 = H.264, 0x03 = AV1
5035
5036        seq:    sequence number per stream per sender (big-endian).
5037
5038    clients handle frame loss gracefully (silent drop, no retransmit).
5039
5040  14.3  audio (VFRAME)
5041
5042    codec selection (normative):
5043
5044      servers MUST support pcm16le and SHOULD support opus.
5045
5046      per-channel consistency: all participants in a channel use the
5047      same audio codec for that session. the codec is selected when
5048      the first participant joins:
5049
5050        - if first joiner specifies codec= and server supports it: use that codec.
5051        - otherwise: server selects from its supported set
5052          (RECOMMENDED: opus if available, else pcm16le).
5053
5054      subsequent joiners receive the already-selected codec in OK.
5055      they MUST accept it or leave the channel.
5056
5057      codec reset: when the last participant leaves (count = 0), the
5058      codec selection clears. the next session re-negotiates.
5059
5060      codec incompatibility: a client that cannot support the channel's
5061      selected audio codec MUST send VOICE op=leave. no ERR is returned
5062      to the client; codec incompatibility is handled by the client
5063      leaving the channel.
5064
5065    payload:
5066
5067      opus:    one Opus packet per VFRAME. 20ms frames, 48kHz mono or stereo.
5068      pcm16le: one 20ms chunk of 48kHz mono 16-bit little-endian PCM.
5069               no codec library required for decoding.
5070
5071  14.4  video (VSTREAM)
5072
5073    codec selection (normative):
5074
5075      servers MUST support vp8. servers SHOULD support h264.
5076      servers MAY support av1.
5077
5078      per-stream flexibility (unlike audio): each client chooses codec
5079      per stream, per frame. there is no per-channel consensus.
5080
5081      example: a client may send camera in VP8 and screen:0 in H.264.
5082
5083    stream identifiers:
5084
5085      camera:     primary camera stream (usual first stream).
5086      screen:0:   first screen share.
5087      screen:1:   second screen share (if client has multiple monitors).
5088      format:     [a-z]+ optionally followed by :[0-9]+
5089
5090    payload:
5091
5092      vp8:  VP8 keyframe or delta frame (RFC 6386 format).
5093      h264: H.264 NAL units in byte-stream format.
5094      av1:  AV1 open bitstream format (OBU).
5095
5096  14.5  encryption and key rotation (requires cap: e2e)
5097
5098    key derivation (normative, enc=1 channels only; cap: e2e required):
5099
5100      uses Sender Keys (Layer 2, Sect. %-section_29_2b-%).
5101      same key derivation as MSG on the same channel.
5102      clients establish Sender Key via X3DH + Sender Key protocol
5103      (full details in Sect. %-section_29_2b-%).
5104
5105    frame encryption:
5106
5107      if enc_flag=0x01 (encrypted channel):
5108
5109        [24 bytes: XChaCha20-Poly1305 nonce (random per frame)]
5110        [N bytes: plaintext payload]
5111        [16 bytes: auth tag]
5112
5113      cipher: XChaCha20-Poly1305 (same as MSG encryption).
5114      nonce: generated fresh per frame (do not reuse).
5115
5116    key version tracking (normative):
5117
5118      clients track key_version for each (uid, cid).
5119      when server generates new Sender Key (member removal),
5120      clients receive update via PUBKEY op=keysync on main WIRE.
5121
5122      frames include implicit key_version (encoded in encryption state).
5123      if a frame's decryption fails (wrong key_version), frame is
5124      silently dropped (no error, no retry). the peer will send a
5125      PUBKEY op=keysync if they detect the receiver is out-of-sync.
5126
5127    key rotation on member removal (normative):
5128
5129      when a member is removed from an enc=1 channel:
5130
5131        1. server generates new Sender Key (fresh seed).
5132        2. server broadcasts PUBKEY op=keysync to all remaining members
5133           (on main WIRE connection).
5134        3. clients derive new Sender Key and update key_version.
5135        4. next frame sent uses new key_version.
5136        5. old frames (from removed member or from before rotation)
5137           decrypt with old key -> decryption fails -> silently dropped.
5138
5139  14.6  participant status (orchestration)
5140
5141    clients announce stream status on the main WIRE connection (orchestration, not frame data):
5142
5143      VSTREAM op=active cid=<cid> sid=<stream-id>
5144      VSTREAM op=idle cid=<cid> sid=<stream-id>
5145
5146    tags:
5147      cid=<cid>: voice channel identifier.
5148      sid=<stream-id>: stream identifier (camera, screen:0, etc).
5149
5150    server broadcasts to all participants with subscriptions in the cid:
5151
5152      VSTREAM op=active cid=<cid> uid=<uid> sid=<stream-id> ts=<unix-ms>
5153      VSTREAM op=idle cid=<cid> uid=<uid> sid=<stream-id> ts=<unix-ms>
5154
5155    op=active:  stream became active (client started sending frames).
5156    op=idle:    stream went idle (client stopped sending frames).
5157
5158    clients use these broadcasts to:
5159      - render gallery view (who has camera active).
5160      - highlight screen shares (show badge "screen share in progress").
5161      - update participant list (who is sending what).
5162      - mark speakers (combined with VOICE op=talk for audio activity).
5163
5164    these are WIRE protocol messages (text, orchestration only).
5165    they require no frame processing.
5166
5167  14.7  stream and connection limits
5168
5169    per-user active streams (normative):
5170
5171      servers SHOULD enforce a maximum number of concurrent streams
5172      per user joining a channel. RECOMMENDED maximum: 3.
5173
5174      when limit is reached, next VSTREAM op=active attempt results in
5175      frame transmission with enc_flag bit set, but server may not relay.
5176      client receives no explicit error (silent drop). this is graceful
5177      degradation, not a protocol error.
5178
5179      typical limits:
5180        1 camera + 2 screen shares
5181        or 2 cameras + 1 screen share
5182        or 1 audio-only + 2 screen shares (accessibility)
5183
5184    per-channel aggregate (implementation note, not normative):
5185
5186      servers may enforce limits on total concurrent video senders per
5187      channel to prevent relay bandwidth exhaustion (if server chooses
5188      to relay). since 0.8 specifies direct P2P, servers do not need
5189      to enforce this, but MAY document it.
5190
5191    connection limits:
5192
5193      servers MAY enforce maximum concurrent voice channel connections
5194      per user (RECOMMENDED: 10). when limit is reached, VOICE op=join
5195      is rejected: ERR code=TOOBIG :maximum voice connections reached.
5196
5197    participant list query (VOICE op=list unchanged):
5198
5199      VOICE op=list cid=<cid>
5200
5201      server replies with current participants:
5202
5203        VOICE op=info cid=<cid> uid=<uid> joined=<unix-ms>
5204        ...
5205        OK verb=VOICE op=list cid=<cid> count=<n> total=<n>
5206
5207      count and total are always equal (no pagination).
5208      servers MUST report actual participant count.
5209
5210%- server_to_server_federation = "Sect.  15" -%
5211========================================================================
5212%-server_to_server_federation-% -- SERVER-TO-SERVER / FEDERATION (EXPERIMENTAL)
5213========================================================================
5214
5215  federation is EXPERIMENTAL. cap: s2s. nothing in this section is
5216  normative for 0.8. it is a design sketch to constrain future revisions.
5217
5218  servers advertising cap=s2s MAY federate with peers.
5219  s2s auth uses Ed25519 pubkey; same state machine as %-section_4_2-%,
5220  method=S2S_PUBKEY.
5221
5222  federated resource paths are prefixed with server hostname:
5223    wire.example.org/community/channel
5224
5225  federated MSG lines carry origin=<hostname> identifying source.
5226
5227  open problems that MUST be resolved before s2s is normative:
5228
5229    mid collision:
5230      two servers generating ULIDs within the same millisecond for
5231      the same channel may produce identical mid values. candidates:
5232      server-namespace prefix on mid (e.g. 'sid:mid'), or reserving
5233      bits in the ULID random component for a server identifier.
5234
5235    ordering:
5236      per-cid ordering guarantee (%-section_6_3-%) cannot be provided
5237      across servers without coordination. last-write-wins is rejected.
5238      causal ordering via explicit parent=<mid> tag is a candidate.
5239
5240    loop prevention:
5241      a message federated from A to B and then back to A must be
5242      detected and dropped. loop detection via origin chain tags is
5243      a candidate.
5244
5245    trust model:
5246      ban state, mute state, and moderation actions do not propagate
5247      automatically across federated servers. cross-server moderation
5248      is undefined.
5249
5250    duplicate handling:
5251      if a server receives a mid it already knows, it MUST silently
5252      drop the message without re-broadcasting.
5253
5254%- capability_negotiation = "Sect.  16" -%
5255========================================================================
5256%-capability_negotiation-% -- CAPABILITY NEGOTIATION
5257========================================================================
5258
5259  CAPS and USE are distinct verbs with distinct directions.
5260
5261  querying server capabilities (before or after AUTH):
5262
5263    CAPS
5264    server replies:
5265    CAPS list=<csv-of-cap-names>
5266
5267  parsers MUST use list= presence to distinguish direction: a CAPS line
5268  carrying list= is always server-sent (the capability reply); a bare
5269  CAPS line with no tags is always client-sent (the query). this is the
5270  same normative MUST pattern used by ATTACH (%-section_12_3-%) for
5271  tag-presence-based dual-direction verb disambiguation.
5272  note: EMOJI (%-section_10_3-%) and KVAL (%-section_7_5-%) use op= value as
5273  the discriminant rather than tag presence; both are valid patterns.
5274  servers MUST NOT send a bare CAPS line; clients MUST NOT send
5275  CAPS list=.
5276
5277  declaring client intent (after AUTH; optional):
5278
5279    USE caps=<csv-of-cap-names>
5280
5281    server replies:
5282    OK verb=USE
5283
5284  USE is advisory. servers MUST still accept all verbs and return
5285  ERR code=UNKNOWN for unimplemented ones. unknown cap names in
5286  USE caps= MUST be ignored by servers. USE may be sent at any time
5287  after AUTH; sending it multiple times is permitted and replaces the
5288  previous declaration.
5289
5290  defined cap names:
5291    voice      -- VOICE extension (%-section_14-%)
5292    s2s        -- federation (%-section_15-%, experimental)
5293    boards     -- board channel type (MSG with title= on type=board)
5294    forums     -- forum channel type (MSG with title= on type=forum)
5295    reactions  -- REACT / UNREACT verbs
5296    threads    -- tid= tag on MSG; thread context
5297    upload     -- UPLOAD / ATTACH verbs
5298    emoji      -- custom emoji (COMM op=emoji.*)
5299    xemoji     -- cross-community emoji (%-section_10_6-%)
5300    kval       -- scoped key-value store (KVAL verb; %-section_7_5-%).
5301                  covers both community branding (scope=comm; %-section_8_6-%)
5302                  and user profile decoration (scope=user; %-section_7_4-%).
5303                  replaces the retired branding and uprofile caps.
5304                  servers not advertising kval MUST return ERR code=UNKNOWN
5305                  on all KVAL lines.
5306    purge      -- HIST op=purge (%-section_29_1-%); server-side permanent
5307                  history deletion. servers that do not advertise purge
5308                  MUST reject HIST op=purge with ERR code=UNKNOWN.
5309    e2e        -- end-to-end encryption (%-section_29_2-%); PUBKEY op=prekey,
5310                  op=prekey.fetch, op=keysync; COMM op=channel.e2e;
5311                  CHAN op=e2e; MSG enc=skdm; e2e= tag on CHAN op=info,
5312                  COMM op=channel.create, and SUB (DM creation).
5313
5314%- error_format = "Sect.  17" -%
5315========================================================================
5316%-error_format-% -- ERROR FORMAT
5317========================================================================
5318
5319  ERR code=<CODE> [verb=<VERB>] [rid=<rid>] [mid=<mid>] :reason
5320
5321  WARN [mid=<mid>] code=<WARNCODE> :reason
5322
5323  ERR signals a rejected or failed operation. WARN signals an accepted
5324  operation that was modified by the server before execution. clients
5325  MUST NOT treat WARN as an error. see %-section_26_0-% for WARN codes.
5326
5327  mid= is present only when the WARN is associated with a specific
5328  message. advisory WARNs (OPK_DEPLETED, SKDM_PENDING) carry no mid=;
5329  parsers MUST handle WARN with mid= absent.
5330
5331  standard ERR codes:
5332
5333    UNKNOWN    -- unrecognized verb
5334    NOPERM     -- permission denied
5335    NOTAUTH    -- not authenticated; verb requires auth (%-section_4_4-%)
5336    NOTFOUND   -- resource does not exist
5337    TOOLONG    -- line exceeds 4096 bytes (%-section_1-%)
5338    AUTHFAIL   -- authentication failed
5339    RATELIMIT  -- rate limit exceeded; retry= tag present
5340    TOOBIG     -- upload size exceeds server limit
5341    BADARG     -- malformed tag value, bad payload encoding,
5342                  missing required tag, or invalid ID format
5343    CONFLICT   -- resource already exists (e.g. duplicate name)
5344    GONE       -- resource existed but was deleted or expired
5345    FORBIDDEN  -- operation is structurally disallowed (distinct from
5346                  NOPERM: NOPERM means the user lacks a permission;
5347                  FORBIDDEN means the operation cannot be performed
5348                  regardless of permission, e.g. sole owner leaving)
5349    CHALLENGE  -- operation requires out-of-band verification before it
5350                  may proceed. url= tag carries a server-provided HTTPS
5351                  URL where the user completes the challenge (web form,
5352                  email link, CAPTCHA, etc.). the protocol carries only
5353                  the redirect; the challenge itself is not on the wire.
5354
5355  reason payload is human-readable and MUST NOT be used by clients
5356  for programmatic logic. clients MUST use code= for control flow.
5357
5358%- rate_limiting = "Sect.  18" -%
5359========================================================================
5360%-rate_limiting-% -- RATE LIMITING
5361========================================================================
5362
5363  servers MUST rate limit per-connection and per-uid.
5364
5365  on limit hit:
5366  ERR code=RATELIMIT verb=<verb> scope=conn|uid retry=<unix-ms> :reason
5367
5368  scope=conn  -- this connection is throttled regardless of uid
5369  scope=uid   -- this uid is throttled globally across all connections
5370
5371  retry is the earliest unix-ms at which the client SHOULD retry the
5372  throttled verb. this is normative: clients MUST NOT retry before retry.
5373
5374  additional normative rules:
5375    - servers MUST implement per-(uid, verb) sliding window tracking.
5376    - servers SHOULD apply stricter limits to MSG than to PING or CAPS.
5377    - servers MAY close the connection after repeated limit violations.
5378
5379  non-normative implementation parameters (server-defined):
5380    - window duration
5381    - message count thresholds per window
5382    - escalation policy
5383
5384%- client_conformance = "Sect.  19" -%
5385========================================================================
5386%-client_conformance-% -- CLIENT CONFORMANCE LEVELS
5387========================================================================
5388
5389  L0  minimal
5390    -- HELLO / AUTH / CAPS / [USE] / MSG / SUB / UNSUB / HIST / PING / IDENTIFY
5391    -- SRVADM op=policy (pre-auth server policy query; %-section_27_1-%)
5392    -- USE is optional at L0; L0 clients MAY omit it and receive all
5393       verbs without declaring capability intent
5394    -- raw text output; no markup rendering
5395    -- escape sequences may be passed through or decoded client-side
5396
5397  L1  text client
5398    -- L0 + TYPING + PRESENCE + REACT + UNREACT + EDIT + DEL
5399    -- KEYROTATE (key rotation; %-section_4_6-%)
5400    -- PING receipt, notification, PING op=list, and PING op=dismiss
5401    -- READSTATE op=mark and op=query (read state tracking)
5402    -- WARN receipt and display (%-section_26_0-%)
5403    -- escape decoding (%-section_2_4-%, step 2)
5404    -- inline markup rendering (%-section_9_2-%) including @mention highlight
5405
5406  L2  feature client
5407    -- L1 + UPLOAD + EMOJI + THREAD verb + PIN + UNPIN
5408    -- KVAL op=set / op=get scope=user (user profile decoration; %-section_7_4-%)
5409    -- role mention rendering with ROLE op=appearance color/emoji (%-section_8_2a-%)
5410    -- block markup rendering (%-section_9_3-%)
5411    -- emoji cache and resolution
5412    -- READSTATE cursor= handling (%-section_6_9-%)
5413    -- CHAN op=* events (channel list, live channel updates)
5414    -- COMM op=channel.list / channel.info (%-section_8_4-%)
5415    -- HIST tid= filter; thread metadata via THREAD op=info
5416
5417  L3  full client
5418    -- L2 + VOICE + board/forum channels (title=, tags= on MSG) + COMM management
5419    -- COMM op=update (community name and open= flag; %-section_8_0-%)
5420    -- COMM op=channel.rename (channel display name update; %-section_8_4-%)
5421    -- LIST verb (%-section_11_5-%): board/forum/text top-level content browsing
5422    -- community branding display, query, and update (KVAL scope=comm;
5423       %-section_8_6-%)
5424    -- full emoji management UI
5425    -- board tag filtering (LIST tags=); forum lock filtering (LIST locked=)
5426    -- invite management (COMM op=invite.list / invite.revoke; INVITE op=info)
5427    -- SRVADM (%-section_27-%)
5428    -- E2E encryption participation: PUBKEY op=prekey publish and fetch,
5429       SKDM distribution and receipt, keysync catch-up, Sender Key
5430       rotation on member removal (%-section_29_2-%)
5431
5432  clients MUST declare level in HELLO:
5433    HELLO version=1 level=<n> :clientname/version
5434
5435  conformance level is informational. servers MUST NOT gate verbs on
5436  level. it is for debugging and operator tooling only.
5437
5438  level= is ephemeral and intentionally unqueryable (normative):
5439    servers MUST NOT persist level= to durable storage. it is a
5440    per-connection hint, not uid-scoped state. there is no query
5441    surface for level= and none is defined. this is the same design
5442    as USE caps= (%-section_16-%): both are client declarations about
5443    rendering intent that affect nothing on the server and therefore
5444    do not constitute "server-maintained state" subject to the
5445    "no hidden state" invariant (%-section_2_6-%). operator tooling
5446    wishing to audit client capabilities MUST use out-of-band logging
5447    of HELLO lines; the protocol provides no query path for this.
5448
5449%- example_session_l0 = "Sect.  20" -%
5450========================================================================
5451%-example_session_l0-% -- EXAMPLE SESSION (L0)
5452========================================================================
5453
5454  client:  HELLO version=1 level=0 :wireshell/1.0
5455  server:  HELLO sid=01JX000000000000000000000 ts=1700000000000
5456               :wire.example.org/0.8
5457  client:  AUTH method=TOKEN uid=01JXAAA000000000000000000 :s3cr3t
5458  server:  OK uid=01JXAAA000000000000000000 :welcome, xplshn
5459  client:  SUB rid=/example/general
5460  server:  OK verb=SUB rid=/example/general cid=01JXBBB000000000000000000
5461  server:  MSG cid=01JXBBB000000000000000000 mid=01JXCCC000000000000000000
5462               uid=01JXDDD000000000000000000 ts=1700000000000 :hello wire
5463  client:  MSG cid=01JXBBB000000000000000000 :hi from shell
5464  server:  OK verb=MSG cid=01JXBBB000000000000000000 mid=01JXEEE000000000000000000 ts=1700000001000
5465  server:  MSG cid=01JXBBB000000000000000000 mid=01JXEEE000000000000000000
5466               uid=01JXAAA000000000000000000 ts=1700000001000 :hi from shell
5467
5468%- example_session_l1 = "Sect.  21" -%
5469========================================================================
5470%-example_session_l1-% -- EXAMPLE SESSION (L1, PUBKEY AUTH + MULTILINE + REACTIONS)
5471========================================================================
5472
5473  // uid shown below is illustrative base32 derived from a pubkey hash.
5474  // actual uids look like any 26-char Crockford base32 string.
5475
5476  // pubkey auth: clean two-line flow; no repeated pubkey
5477  client:  HELLO version=1 level=1 :wireclient/0.8
5478  server:  HELLO sid=01JX000000000000000000000 ts=1700000000000
5479               :wire.example.org/0.8
5480  client:  AUTH method=PUBKEY uid=7ZNKQ5XMVP2R4ABCDE01JXAAA :AAAA...pubkey
5481  server:  CHALLENGE nonce=a3f9e2d1c0b7a6f5...64hexchars...9e8d7c6b
5482  // client signs: nonce_bytes(32) || "01JX000000000000000000000"(26) ||
5483  //               ts_uint64_be(8) = 66 bytes total
5484  client:  AUTH method=PUBKEY sig=BBBB...base64url-sig-no-padding
5485  server:  OK uid=7ZNKQ5XMVP2R4ABCDE01JXAAA :welcome
5486
5487  // declare caps intent
5488  client:  USE caps=reactions,threads,upload
5489  server:  OK verb=USE
5490
5491  // first-time display name
5492  client:  IDENTIFY name=xplshn
5493  server:  OK verb=IDENTIFY uid=7ZNKQ5XMVP2R4ABCDE01JXAAA ts=1700000001000
5494  server:  PROFILE uid=7ZNKQ5XMVP2R4ABCDE01JXAAA name=xplshn ts=1700000001000
5495
5496  // subscribe and multiline message
5497  client:  SUB rid=/example/general
5498  server:  OK verb=SUB rid=/example/general cid=01JXBBB000000000000000000
5499  client:  MSG cid=01JXBBB000000000000000000 :line one\nline two
5500  server:  OK verb=MSG cid=01JXBBB... mid=01JXFFF... ts=1700000002000
5501  server:  MSG cid=01JXBBB... mid=01JXFFF... uid=7ZNKQ5XMVP2R4ABCDE01JXAAA
5502               ts=1700000002000 :line one\nline two
5503  // OK arrives first with the server-assigned mid; broadcast follows
5504
5505  // mark read: originating session gets OK; other sessions get broadcast
5506  client:  READSTATE op=mark mid=01JXFFF000000000000000000 cid=01JXBBB000000000000000000
5507  server:  OK verb=READSTATE op=mark mid=01JXFFF000000000000000000 cid=01JXBBB000000000000000000
5508  // (other session of same uid receives the READSTATE broadcast separately)
5509
5510  // reaction
5511  client:  REACT mid=01JXFFF000000000000000000 eid=thumbsup
5512  server:  OK verb=REACT mid=01JXFFF... cid=01JXBBB... eid=thumbsup ts=1700000003000
5513  server:  REACT mid=01JXFFF... cid=01JXBBB... uid=7ZNKQ5XMVP2R4ABCDE01JXAAA
5514               eid=thumbsup name=thumbsup ts=1700000003000
5515  // broadcast carries cid= (self-identifying per %-section_2_5-%)
5516
5517  // history (reactions included; EDIT would carry rev= and cid= if present)
5518  client:  HIST cid=01JXBBB000000000000000000 limit=10
5519  server:  MSG cid=01JXBBB... mid=01JXFFF... uid=7ZNKQ5XMVP2R4ABCDE01JXAAA
5520               ts=1700000002000 :line one\nline two
5521  server:  REACT mid=01JXFFF... cid=01JXBBB... uid=7ZNKQ5XMVP2R4ABCDE01JXAAA
5522               eid=thumbsup name=thumbsup ts=1700000003000
5523  server:  OK verb=HIST cid=01JXBBB... count=2 has_more=0
5524
5525  // query pending PINGs explicitly
5526  client:  PING op=list gid=01JXGGG000000000000000000
5527  server:  PING mid=01JXHHH... cid=01JXBBB... gid=01JXGGG... uid=01JXDDD...
5528               ts=1700000000500 kind=user
5529  server:  OK verb=PING op=list count=1 total=1
5530
5531%- example_session_l2 = "Sect.  22" -%
5532========================================================================
5533%-example_session_l2-% -- EXAMPLE SESSION (L2, CHANNEL DISCOVERY + RECONNECT)
5534========================================================================
5535
5536  // demonstrates two scenarios:
5537  //   A) first-time connect: discover channels and subscribe.
5538  //   B) reconnect: server restores subscriptions automatically; client
5539  //      only needs to fetch history from last read cursor.
5540
5541  // auth phase (abbreviated; see %-section_21-% for full pubkey flow)
5542  client:  HELLO version=1 level=2 :wireclient/0.8
5543  server:  HELLO sid=01JX000000000000000000000 ts=1700000010000
5544               :wire.example.org/0.8
5545  // ... AUTH handshake ...
5546  server:  OK uid=7ZNKQ5XMVP2R4ABCDE01JXAAA :welcome
5547  // server sends any pending PINGs immediately after OK, then restores
5548  // subscriptions: broadcasts are live on all previously subscribed channels
5549  // before the client issues any verb.
5550
5551  // SCENARIO A: first-time client (no prior subscriptions)
5552
5553  // step 1: find communities I belong to
5554  client:  COMM op=list
5555  server:  COMM op=info gid=01JXGGG000000000000000000 slug=example
5556               name=Example%20Community open=0 members=42
5557               owner=01JXZZZ000000000000000000 ts=1700000000000
5558  server:  OK verb=COMM op=list count=1 total=1
5559
5560  // step 2: discover channels
5561  client:  COMM op=channel.list gid=01JXGGG000000000000000000
5562  server:  CHAN op=info gid=01JXGGG... cid=01JXBBB... slug=general
5563               name=general type=text e2e=0 ts=1700000000000
5564  server:  CHAN op=info gid=01JXGGG... cid=01JXCCC... slug=announcements
5565               name=announcements type=text e2e=0 ts=1699000000000 :Welcome to Example
5566  server:  OK verb=COMM op=channel.list gid=01JXGGG... count=2 total=2
5567
5568  // step 3: subscribe --- durable; server will restore on next connect
5569  client:  SUB cid=01JXBBB000000000000000000
5570  server:  OK verb=SUB rid=/example/general cid=01JXBBB000000000000000000
5571  client:  SUB cid=01JXCCC000000000000000000
5572  server:  OK verb=SUB rid=/example/announcements cid=01JXCCC000000000000000000
5573
5574  // step 4: fetch history
5575  client:  HIST cid=01JXBBB000000000000000000 limit=50
5576  server:  MSG cid=01JXBBB... mid=01JXFFF... uid=... ts=... :hello wire
5577  server:  OK verb=HIST cid=01JXBBB... count=1 has_more=0
5578
5579  // SCENARIO B: reconnect (subscriptions already stored server-side)
5580
5581  // after AUTH OK and PING burst, server has already restored broadcasts
5582  // on 01JXBBB and 01JXCCC. client only needs to catch up on missed messages.
5583
5584  // step 1: get read cursors
5585  client:  READSTATE op=query gid=01JXGGG000000000000000000
5586  server:  READSTATE op=query cid=01JXBBB... count=3
5587               oldest=01JXFFF... cursor=01JXEEE...
5588  server:  READSTATE op=query cid=01JXCCC... count=0
5589  server:  OK verb=READSTATE op=query gid=01JXGGG...
5590
5591  // step 2: fetch missed messages from cursor
5592  client:  HIST cid=01JXBBB000000000000000000 after=01JXEEE... limit=50
5593  server:  MSG cid=01JXBBB... mid=01JXFFF... uid=... ts=... :first unread
5594  server:  MSG cid=01JXBBB... mid=01JXGGG2... uid=... ts=... :second unread
5595  server:  MSG cid=01JXBBB... mid=01JXHHH2... uid=... ts=... :third unread
5596  server:  OK verb=HIST cid=01JXBBB... count=3 has_more=0
5597
5598  // no local storage was used. all state was recovered from server queries.
5599
5600%- implementation_notes = "Sect.  23" -%
5601========================================================================
5602%-implementation_notes-% -- IMPLEMENTATION NOTES
5603========================================================================
5604
5605  Go reference implementations for key primitives (key generation,
5606  uid derivation, payload unescaping, line parsing, server data store
5607  sketch) are in IMPL.md. all examples are non-normative.
5608
5609%- design_rationale = "Sect.  24" -%
5610========================================================================
5611%-design_rationale-% -- DESIGN RATIONALE
5612========================================================================
5613
5614  why mentions are structural tags and not parsed from markup?
5615    if PINGs depended on parsing @uid:name markup, every server would
5616    need a markup parser on the hot path, violating the dumb-relay
5617    design. separating mention metadata (tags) from mention rendering
5618    (markup) keeps servers dumb. the sender declares who they mention;
5619    the server validates membership and delivers PINGs. an L0 minimal client
5620    that never renders markup still participates correctly by including
5621    the tags. the body and the tags are allowed to diverge; the protocol
5622    does not enforce consistency between them. this is intentional.
5623
5624  why PING is out-of-band and not in the channel stream?
5625    a user subscribed to 50 channels would have to consume all channel
5626    traffic to find the 2 messages that mentioned them. PING delivers
5627    the needle directly. the client then fetches context via HIST only
5628    if needed. this inverts the correct flow: mention delivery is O(1)
5629    per recipient, not O(channel-volume).
5630
5631  why READSTATE instead of SEEN/UNSEEN?
5632    SEEN (mark) and UNSEEN (query) were two verbs operating on the same
5633    state from opposite directions with asymmetric naming. READSTATE
5634    op=mark / op=query is a single verb, single state surface, explicit
5635    direction via op=. the same pattern used by PING op=list and COMM
5636    op=*. orthogonality over naming convenience.
5637
5638  why READSTATE op=mark sends OK to the originator, not just a broadcast?
5639    the originating session needs to confirm the server accepted the mark
5640    before it can safely update its local unseen count. if only the
5641    broadcast were sent (to other sessions), the originating session would
5642    have no confirmation: it could not distinguish "server accepted" from
5643    silence caused by a dropped connection. every other mutating verb has
5644    an OK. READSTATE op=mark is consistent with that convention. the
5645    broadcast to other sessions is sent after the OK, as with all
5646    multi-target operations.
5647
5648  why WHO returns PROFILE?
5649    WHO (pull) and IDENTIFY-triggered broadcast (push) both describe the
5650    same entity state. using two verbs (USER for pull, PROFILE for push)
5651    means clients need two handlers for identical data. a single PROFILE
5652    verb with consistent fields means one handler, one cache path,
5653    regardless of whether the data was requested or delivered
5654    unsolicited. KEYROTATE also broadcasts PROFILE with prev_uid= so
5655    clients can merge cache entries on key rotation.
5656
5657  why CAPS query / USE declare are separate verbs?
5658    CAPS with no args and CAPS use=... shared a verb but had opposite
5659    directionality: one is a server query, one is a client declaration.
5660    same verb, different semantics, distinguished by tag presence alone.
5661    USE caps=... is unambiguous. CAPS remains a pure server query.
5662    the split costs one additional verb name and gains clarity.
5663
5664  why ROLE is the single verb for all role operations?
5665    CHAN uses one verb for both query replies (CHAN op=info) and events
5666    (CHAN op=create, op=delete, etc.). EMOJI does the same. the earlier
5667    design used ROLEAPP as a separate event verb alongside ROLE for
5668    queries, which broke this pattern. a client parser dispatches on
5669    verb first; if role events arrive as ROLEAPP while queries come
5670    back as ROLE, the client needs two dispatch paths for the same
5671    logical entity. unifying under ROLE op=* means one dispatch path,
5672    one cache-update handler, no confusion about which verb carries
5673    which kind of event. the ROLEAPP name is retired.
5674
5675  why ROLE op=create is broadcast (not just the OK to the requester)?
5676    without ROLE op=create, a client that is online when a new role is
5677    created only learns of it when it receives a ROLE op=assign or
5678    ROLE op=appearance for that rolid --- at which point it must do a
5679    COMM op=role.list round-trip to learn the role name. this is the
5680    same problem that motivated CHAN op=create broadcasts. the fix is
5681    the same: broadcast on creation so all connected clients stay
5682    coherent without a round-trip. ROLE op=create carries name, color,
5683    and perms --- everything needed to cache the role fully.
5684
5685  why CHAN for channel events and queries?
5686    channel management (COMM op=channel.*) writes; channel events and
5687    queries are read-path. CHAN op=info is the query reply (mirrors ROLE
5688    op=info and MEMBER op=info). CHAN op=create/delete/rename/topic/perm
5689    are broadcasts (mirrors ROLE op=create/delete, MEMBER, KVAL). this
5690    gives every entity type the same two-concern split: one management
5691    verb (COMM) and one read/event verb (CHAN / ROLE / MEMBER / etc).
5692    the pattern is consistent and composable.
5693
5694  why COMM op=update broadcasts on COMM instead of a separate event verb?
5695    every other community entity has a dedicated event verb: CHAN, ROLE,
5696    MEMBER, EMOJI, KVAL, INVITE. COMM op=update is the only place where
5697    the management verb also carries the broadcast. this is an intentional
5698    design exception. introducing a COMMUNITY event verb for a single op
5699    would expand the verb surface without orthogonality gain. the uid=
5700    discriminant makes direction unambiguous. the exception is documented
5701    here so that implementors do not mistake it for a pattern to repeat;
5702    all future entity additions MUST follow the COMM/EVENT split.
5703
5704  why only role and emoji get pre-SUB-OK cache-warm pushes?
5705    the cache-warm push principle (%-section_8_2a-%) restricts pre-SUB-OK pushes to
5706    entity data required for rendering incoming MSG lines: roles (mention
5707    display, hoisting, colour) and emoji (inline emoji rendering). branding,
5708    member lists, channel permission lists, and channel lists are NOT
5709    required to render a message line. pushing them pre-SUB-OK would make
5710    subscribe latency a function of community size (unbounded). those
5711    entities are queried on demand. the CHAN op=perm push MAY be included
5712    in the pre-SUB-OK burst (%-section_8_4-%) because permission state is needed
5713    to render the correct input UI before the first message arrives; this
5714    is a narrow, bounded set (one CHAN op=perm line per role override).
5715
5716  why COMM op=channel.perm.list exists (was not in pre-0.8 versions)?
5717    channel-level permission overrides (CHAN op=perm) were broadcast as
5718    live events but had no query surface. a reconnecting client that missed
5719    the broadcast could not reconstruct the override state. the protocol's
5720    core invariant is that state is fully reconstructible from query +
5721    event stream (%-section_24-% event log model). channel perm overrides violated
5722    that invariant. COMM op=channel.perm.list closes the gap. its reply
5723    format (CHAN op=perm lines without uid=) reuses the existing broadcast
5724    format minus the event-specific uid= and ts=, keeping the verb set DRY.
5725
5726  why SRVADM op=perm.grant/revoke instead of op=grant/revoke?
5727    SRVADM has two conceptually different operations: role assignment
5728    (op=role.assign / op=role.revoke, mirrors COMM op=role.assign) and
5729    per-uid permission grants (op=perm.grant / op=perm.revoke for
5730    community.create). the original op=grant / op=revoke names were
5731    ambiguous --- they looked like they could apply to roles too. the perm.
5732    namespace prefix distinguishes role operations from direct permission
5733    operations and makes the asymmetry explicit: server.admin is a role
5734    (assigned/revoked); community.create is a per-uid flag (granted/revoked).
5735    future per-uid server permissions would use op=perm.grant/revoke as well.
5736
5737  why SRVADM op=info uid=<own-uid> is permitted without server.admin?
5738    a uid granted community.create (but not server.admin) has server-level
5739    state (their own grant record) that directly affects protocol behaviour
5740    (whether COMM op=create succeeds). without the self-query exception,
5741    that uid has no protocol path to confirm the grant is present, active,
5742    or has been revoked. this is a clean violation of the "no hidden state"
5743    principle (%-section_2_6-%). the exception is narrow: it permits a uid to inspect
5744    only its own SRVADM record, which contains no other uid's data. it does
5745    not permit calling SRVADM op=list, SRVADM op=role.assign, or any other
5746    SRVADM mutating op without server.admin. the self-query exception
5747    follows the same principle as WHO uid=<self>, which returns the real
5748    (possibly invisible) presence status to the owning uid without general
5749    presence-inspection permission.
5750
5751  why HIST EDIT lines must carry rev=?
5752    a client replaying HIST to reconstruct message state needs to know
5753    whether an EDIT is the latest revision or an intermediate one. in
5754    practice HIST returns only the latest revision body, but rev= allows
5755    the client to validate this and handle any future history-expansion
5756    extension without protocol changes. omitting rev= from HIST EDIT
5757    was an oversight; it is now normative.
5758
5759  why HIST has has_more=?
5760    count alone (number of MSG lines) was ambiguous for pagination: a
5761    result of count=2 could be a reaction-dense window with 2 messages
5762    and 48 events, or a sparse window at the beginning of history. the
5763    client cannot distinguish "there's more" from "you've hit the end"
5764    without knowing total event count relative to the requested limit.
5765    has_more=0|1 is a single bit that answers the only question the
5766    client actually has: keep paginating or stop?
5767
5768  why does bare HIST (no anchor) return the NEWEST messages?
5769    a client opening a channel always wants to see the most recent
5770    messages. returning the oldest messages by default would require
5771    every client to fabricate a sentinel mid at the supremum of the ULID
5772    space just to get the initial view --- a fragile dependency on
5773    implementation details of the ID encoding. "newest by default" is
5774    the correct and only useful default. scroll-up pagination via
5775    before=<oldest-loaded-mid> is the natural complement.
5776
5777  why seen state is client-reported and not inferred from delivery?
5778    delivery does not imply reading. the client knows when the user
5779    views content; the server does not. server-side inference (seen on
5780    delivery) is wrong for any non-trivial client. explicit READSTATE
5781    op=mark from the client is the only correct model. READSTATE is
5782    also intentionally NOT broadcast to other channel members; it is
5783    private per-uid state, synced only to other sessions of the same uid.
5784
5785  why COMM op=create returns default_cid=?
5786    the server atomically creates a general channel on community creation.
5787    without default_cid= in the OK, the creator must immediately issue
5788    COMM op=channel.list just to find the channel they were just placed
5789    into. this is a wasted round-trip for information the server already
5790    has. including default_cid= in the creation reply closes the loop
5791    at zero cost: one OK line, client is immediately ready to SUB.
5792
5793  why COMM op=role.create returns rolid=?
5794    rolids are server-assigned ULIDs. the client has no way to learn the
5795    rolid of a role it just created without a follow-up COMM op=role.list.
5796    returning rolid= in the OK is consistent with how aid= is returned
5797    from emoji.add and cid= from channel.create. the client needs the id
5798    immediately to do anything with the new entity.
5799
5800  why identity-based uid (key-derived, not server-assigned)?
5801    a server-assigned uid requires the server to exist before identity
5802    does. a key-derived uid means the identity exists before any server
5803    knows about it. this enables:
5804      - same uid across all servers without coordination
5805      - zero-step "registration": first auth creates the account
5806      - federation: messages from uid X mean the same person everywhere
5807      - key rotation without losing identity (%-section_4_6-% alias chain)
5808    the SHA-256 + base32 derivation is trivial to implement in any
5809    language with a standard crypto library. it is not a ULID (no time
5810    component) but fits in the same 26-char slot.
5811
5812  why include sid and ts in the signed challenge message?
5813    nonce alone prevents replay within the TTL window. adding sid binds
5814    the signature to this specific server: a signature captured from
5815    server A cannot be replayed against server B even if they share a
5816    nonce collision. adding ts (the server's own timestamp, not the
5817    client's) binds it to the time window without requiring the client
5818    to have a synchronized clock. the client signs what the server told
5819    it; the server verifies using its own values. no clock trust required.
5820
5821  why two AUTH lines instead of one?
5822    the challenge-response pattern is necessary for security: the server
5823    must present fresh randomness before the client signs. a single-line
5824    auth would require the client to sign a value it chooses (insecure)
5825    or repeat the pubkey in the second line (what 0.2 did, redundant).
5826    0.3 removes the redundancy: pubkey in line 1, sig in line 2, no
5827    repeat. the server binds them via the connection state machine.
5828
5829  why token auth at all if pubkey is the primary method?
5830    tokens serve real use cases that key-based auth handles poorly:
5831      - bot accounts running on servers without key storage
5832      - session tokens so the long-term key is not repeatedly used
5833      - API access from shell scripts (no crypto primitives needed)
5834    tokens are always tied to a uid that has a keypair backing it.
5835    they are second-class credentials, not primary identity.
5836
5837  why line-oriented?
5838    line-oriented protocols are trivially debuggable: netcat or a Go
5839    bufio.Scanner is sufficient. no parser library needed; a single
5840    strings.Index call splits the payload. log files are human-readable.
5841
5842  why ULID?
5843    time-sortable, globally unique, no coordination, no UUID noise.
5844    prefix scans replace range queries.
5845
5846  why server-assigned ULIDs?
5847    clock skew and adversarial clients. the server controls sort order
5848    and enforces the monotonic increment rule.
5849
5850  why \n escaping instead of percent-encoding for payload?
5851    percent-encoding (%0A) is correct for tag values where the valid
5852    character set is tightly constrained. for human-readable message
5853    bodies, \n is the universal convention. it reads better in raw
5854    logs. it requires less allocation. percent-encoding payload would
5855    create a second encoding context that conflicts with how operators
5856    read wire traffic. \n wins on legibility and convention.
5857
5858  why deny-wins in permission resolution?
5859    explicit deny must mean deny. if allow-wins, revoking a permission
5860    granted by a lower-priority role requires editing that role.
5861    deny-wins makes revocation a local operation on the denying role.
5862    community.admin is the only absolute override, and it is explicit.
5863
5864  why not aggregate reactions server-side?
5865    aggregation is a rendering decision. a server that aggregates
5866    introduces server-side rendering state, which contradicts the
5867    transparent/dumb-relay design. clients replay REACT/UNREACT from
5868    HIST and build counts locally.
5869
5870  why does HIST tid= on text channels include the root message via
5871  retroactive annotation rather than requiring clients to fetch it
5872  separately?
5873    when a text-channel thread is created implicitly (%-section_11_3-%),
5874    the root message has already been broadcast without tid=. if HIST
5875    tid= did not include the root, clients would need a separate HIST
5876    or fetch for every thread view -- a wasted round-trip for data the
5877    server already has. retroactively annotating the stored root record
5878    with tid= is a bounded server-side mutation (one field, one record,
5879    at implicit creation time) that eliminates this penalty. the
5880    live-vs-HIST divergence (live broadcast has no tid=; HIST does) is
5881    accepted as the lesser cost. clients that received the root live can
5882    identify it as the thread opener when they receive the first reply
5883    (its mid == the tid of the reply). clients that only see HIST get
5884    the complete thread in one request.
5885
5886  why does VOICE op=join support a client codec preference?
5887    the original design had the server pick the codec unilaterally.
5888    a minimal client (pcm16le only, no Opus) on a server that defaults
5889    to opus would fail to join any voice channel unless it happened to
5890    join first. adding [codec=<preferred-codec>] to the send form lets
5891    the client express capability without requiring a separate
5892    negotiation verb. the server retains final authority (OK codec=
5893    carries the actual selection). the invariant that servers MUST
5894    support pcm16le ensures any conforming client can always join.
5895    per-channel codec locking (all participants share one codec)
5896    avoids server-side transcoding, which would be expensive and
5897    contradict the dumb-relay design.
5898
5899  why markup stored verbatim?
5900    servers are routers, not renderers. a dumb client gets readable
5901    raw text. no server-side HTML ever touches the wire.
5902
5903  why no binary framing?
5904    framing adds complexity. attachments use HTTP. the chat bus does
5905    one thing: route text lines.
5906
5907  why caps advisory and not mandatory?
5908    mandatory caps create negotiation deadlocks and break minimal
5909    clients. advisory caps let clients declare intent without the
5910    server gatekeeping on declared level.
5911
5912  why no bulk subscriber list verb (like IRC NAMES)?
5913    IRC needs NAMES because the server is the sole source of truth for
5914    channel membership. WIRE inverts this: identity is key-derived and
5915    server-agnostic; channels have subscribers (transient), not members
5916    (durable). durable membership is a community concept (gid). clients
5917    build their uid cache organically: lazy WHO on first encounter,
5918    reactive PROFILE updates on identity change. a bulk subscriber
5919    snapshot would be stale immediately and tempts clients to use it as
5920    a presence oracle. community membership enumeration is COMM
5921    op=member.list (available to any community member, paginated).
5922
5923  why READSTATE cursor= instead of a separate SYNC verb?
5924    SYNC was a second durable store for the "last read position" per
5925    (uid, cid), motivated by READSTATE's seen-bits being per-message
5926    rather than per-channel. but READSTATE op=mark with upto= already
5927    makes seen-state channel-level: the server records the high-water
5928    mid. that high-water mid IS the cursor. adding cursor= to
5929    READSTATE op=query exposes data the server already holds from the
5930    same store, in the same verb, with no second store and no new cap.
5931    a client reconnecting after being offline issues
5932    READSTATE op=query gid=<gid> and gets count + oldest + cursor for
5933    every channel in one sweep. SYNC was solving a convenience problem
5934    (one round-trip resume) that cursor= solves identically. removing
5935    SYNC eliminates a cap, a section, a store, and a rationale paragraph
5936    defending its existence.
5937
5938  why no reference definitions for role appearance?
5939    reference definitions (CommonMark-style [id]: scheme:value in message
5940    bodies) were intended to let message authors suggest per-message
5941    styling for roles and users. but ROLE op=appearance already delivers
5942    server-authoritative role appearance to clients on SUB, and
5943    COMM op=role.list covers cache misses. an in-body styling mechanism
5944    that is always overridden by ROLE op=appearance data adds parsing
5945    burden to every message in every client without adding correctness.
5946    the @rolid:name markup already carries a display hint for L0 clients.
5947    two mechanisms (ROLE op=appearance for authority, name hint for L0)
5948    are sufficient. a third is not needed.
5949
5950  why EDIT, DEL, REACT, and UNREACT OKs carry cid=?
5951    every mutating verb has an OK. MSG got its OK in 0.8 for exactly
5952    this reason (see "why MSG has an OK response?"). EDIT and DEL were
5953    the remaining exceptions. the same argument applies: the originating
5954    client has no reliable confirmation until the broadcast arrives, and
5955    correlating the broadcast to a sent EDIT or DEL requires matching by
5956    mid (since there is nothing else to correlate on). an OK carrying
5957    rev= (for EDIT) or ts= (for DEL) closes the loop at zero cost and
5958    makes every mutating verb uniformly confirmable. consistency is
5959    worth more than the two extra OK lines per operation.
5960
5961    cid= is included in all four OKs (EDIT, DEL, REACT, UNREACT) for
5962    the same reason it is included in MSG OK: a client pipelining these
5963    operations across multiple channels cannot route the OK without
5964    pending-request state unless the OK carries cid=. the server derives
5965    cid from its mid->cid index at validation time --- it already performs
5966    this lookup to authorize the operation --- so echoing cid= in the OK
5967    costs nothing. this makes all four OKs self-routing, consistent with
5968    MSG OK and every other operation in the protocol.
5969
5970  why attachments= on MSG instead of a separate ATTACH event in HIST?
5971    a client rendering an inline image in a live MSG or in HIST needs
5972    the MIME type to choose a renderer (image/webp vs image/gif) or
5973    to decide whether to show a file icon rather than inline-render.
5974    the only current path is a separate ATTACH aid= round-trip per
5975    attachment, per message. for a channel with 50 messages each
5976    carrying one attachment, that is 50 extra round-trips before the
5977    feed is renderable. attachments= mirrors the mentions= pattern
5978    exactly: structural metadata carried alongside the body so the
5979    client has everything it needs in the message line itself. servers
5980    already store mime= at UPLOAD time; surfacing it here costs nothing.
5981    ATTACH aid= remains the correct path for resolving a download URL
5982    (which is time-limited and must not be cached); attachments= only
5983    carries the type information needed for rendering decisions.
5984
5985  why mime= on EMOJI lines?
5986    emoji assets are typed: a static PNG, an animated GIF, an animated
5987    WebP, and a static WebP are all different rendering cases. the
5988    animated= boolean collapses them to a single bit, which is
5989    sufficient for the common case but forces clients that care about
5990    the distinction (e.g. for codec selection or fallback) to fetch the
5991    asset URL to read the Content-Type header. mime= makes the type
5992    explicit at cache-fill time (EMOJI op=info on SUB or emoji.list)
5993    with no extra round-trip. animated= is retained as a convenience
5994    shortcut; it is derivable from mime= but saves clients that only
5995    need the boolean from implementing MIME subtype parsing.
5996
5997  why UPLOAD OK echoes name=, size=, mime=?
5998    the client sends these values to propose what it is uploading. the
5999    server may normalize or reject them (e.g. reject a text/plain mime
6000    for an image-only upload slot). echoing the server-canonical values
6001    back in the OK gives the client a single confirmation that what it
6002    sent was accepted as-is, without requiring a follow-up ATTACH query
6003    before the HTTP PUT. this is the same principle as COMM op=create
6004    returning default_cid= and COMM op=role.create returning rolid=:
6005    the client should not need a round-trip to learn the canonical form
6006    of data it just submitted.
6007
6008  why READSTATE uses tag absence instead of the 'none' sentinel?
6009    the grammar (%-section_2_1-%) defines tag values as 1*TCHAR: a tag
6010    value MUST be at least one character. an absent value is expressed
6011    by omitting the tag, not by encoding it as the string 'none'.
6012    using 'none' as a sentinel conflates "tag is present with a
6013    placeholder value" with "tag is absent". receivers that follow the
6014    grammar already treat absent tags as having no value; 'none' adds
6015    a second code path for the same semantic. tag absence is the
6016    canonical representation of no-value throughout the protocol
6017    (optional tags on MSG, EMOJI, PROFILE, etc.); READSTATE should
6018    follow the same rule.
6019
6020  why COMM op=list includes owner= in COMM op=info lines?
6021    the list form and the single-query form describe the same entity.
6022    omitting owner= from the list form forces clients that need
6023    ownership information (e.g. to display a crown, to decide whether
6024    to offer a "transfer ownership" action) to issue a COMM op=info
6025    for each community after listing. this is a needless N+1 pattern.
6026    the owner= field is a single uid stored in the community record;
6027    including it in list responses costs one tag per line and eliminates
6028    the follow-up queries entirely.
6029
6030  why THREAD verb instead of COMM op=thread.*?
6031    COMM is community-scoped management (gid=). thread operations are
6032    channel-scoped (cid= + tid=). mixing them under COMM would require
6033    gid= on every THREAD op just to route the permission check, even
6034    though the server can derive gid from cid. a separate THREAD verb
6035    keeps the scope correct and avoids polluting COMM with non-community
6036    operations. the pattern is consistent: MSG for content, CHAN for
6037    channel metadata, THREAD for thread metadata, COMM for community
6038    structure.
6039
6040  why tid= on text channels uses mid-of-root as tid?
6041    a thread on a text channel is started by a message with no parent.
6042    the natural stable identifier for that thread is the message itself.
6043    requiring the client to remember a separate tid= assigned by the
6044    server (as on forum channels) would add a round-trip: send MSG,
6045    wait for OK, learn tid, then others can reply. using mid-as-tid
6046    allows the client to start accepting replies as soon as it has
6047    its own mid= from the MSG OK, with no second round-trip. forum
6048    channels need a server-assigned tid= because the tid= IS the thread
6049    and it must be stable even if the opening message is deleted;
6050    text-channel threads are ephemeral reply chains where this
6051    distinction does not matter.
6052
6053  why LIST has tags= filter?
6054    the purpose of tags= on board posts is discoverability. a LIST
6055    that cannot filter by tag forces every client to download all posts
6056    and filter client-side --- defeating the point of tags entirely. the
6057    server already stores tags= on each MSG; the filter is a prefix
6058    scan on the indexed tag set, not a full-table scan. it costs nothing
6059    to expose. for forums, locked= fills the analogous role: a moderator
6060    or a user who only wants open threads should not have to download
6061    all threads to separate them.
6062
6063  why HIST has tid= filter?
6064    without it, fetching all replies in a forum thread requires downloading
6065    the entire channel history and filtering client-side by tid=. this
6066    is O(channel) instead of O(thread). for a forum with thousands of
6067    threads, the difference is decisive. tid= on HIST is a server-side
6068    range scan on the (cid, tid) index, which the server must maintain
6069    anyway to enforce the tid= tag on broadcast delivery. no new storage
6070    is needed; the filter just exposes what is already indexed.
6071
6072  why board posts cannot be replied to with tid=?
6073    board channels are a flat list of standalone posts, analogous to
6074    a subreddit or bulletin board. each post is its own unit. allowing
6075    tid= on board channels would create an ambiguous model: is a reply
6076    a nested comment (not visible in LIST) or a separate post? the
6077    spec avoids this by making board posts strictly flat and using
6078    ref= for the "quote this post" pattern. if a community wants nested
6079    discussion on board posts, type=forum is the correct channel type.
6080
6081  why THREAD op=unlock requires thread.manage only for moderator locks?
6082    the original spec required thread.manage for all unlocks, which
6083    trapped authors: self-close a question, realize it was a mistake,
6084    unable to reopen without a moderator. the fix is to track locked_by
6085    on the server. if locked_by == requesting uid, the author locked it
6086    and can freely reopen it. if locked_by != requesting uid, a moderator
6087    locked it and the author cannot override --- only someone with
6088    thread.manage can. this is exactly the right model: self-service
6089    closure with protected moderator locks. the locked_by field is a
6090    single uid stored in the thread record; no additional store needed.
6091
6092  why no POST verb? why does MSG carry title= and tags= instead?
6093    a board post is a MSG with a title and optional tags on a type=board
6094    channel. a forum thread opener is a MSG with a title on a type=forum
6095    channel where the server assigns the tid=. the structured metadata
6096    (title=, tags=) is carried as tags on MSG, exactly as mentions= and
6097    rolmentions= are. separate verbs POST and THREAD would have duplicated
6098    the MSG broadcast path with no new semantics. one verb (MSG) composes
6099    with channel type to produce the correct behaviour. LIST replaces the
6100    old POST/THREAD listing verb with a uniform interface across channel
6101    types.
6102
6103  why invisible broadcasts as offline to others?
6104    the entire purpose of invisible is to appear offline to others while
6105    remaining connected. if the server does not explicitly broadcast
6106    status=offline on transition to invisible, any subscriber who already
6107    cached the user's prior status (online, idle, etc.) will see a stale
6108    status until their next reconnect. broadcasting status=offline
6109    immediately on PRESENCE status=invisible ensures all subscribers
6110    converge to the correct view instantly. the server stores the real
6111    status internally only; it never emits 'invisible' to anyone but
6112    the owning uid's own sessions. the 'here' role excludes invisible
6113    users for the same reason: they are offline as far as the community
6114    is concerned.
6115
6116  why board.manage replaces msg.delete/msg.pin on board channels?
6117    msg.delete and msg.pin are chat-channel concepts operating on a
6118    stream of messages. board channels are a flat post list; the
6119    moderation model is different (boards have curators, not message
6120    moderators). using a separate board.manage permission allows a
6121    community to have, for example, a "board editor" role with
6122    board.manage but no msg.delete on text channels --- a clean separation
6123    of moderation scopes. the rule is simple: on board channels,
6124    board.manage is the sole gating permission for actions on others'
6125    content; msg.delete and msg.pin are not consulted for that case.
6126
6127  why CHAN op=info carries topic as payload, not a tag?
6128    topics are wire markup and can contain \n-encoded newlines. tags
6129    must fit within the 4096-byte line limit alongside name=, type=,
6130    cid=, gid=, ts=. markup in a tag also requires percent-encoding,
6131    making raw logs unreadable. payload is the correct location for
6132    human-readable markup: it is always the last field, not subject to
6133    TCHAR restrictions, and trivially extractable by shell tooling via
6134    the ' :' delimiter. this is consistent with MSG body, THREAD
6135    op=rename title, and all other markup-bearing content in the spec.
6136
6137  why INVITE list replies carry op=info?
6138    every entity list in the protocol follows VERB op=info...:
6139      CHAN op=info / ROLE op=info / MEMBER op=info / EMOJI op=info
6140    a bare INVITE gid= code= without op= breaks this pattern and forces
6141    parsers to special-case the INVITE verb. op=info costs one tag and
6142    makes INVITE consistent: dispatch on verb, then dispatch on op=.
6143    no special cases anywhere in the parser.
6144
6145  why emoji.list needs pagination?
6146    a community with thousands of custom emoji sending an unbounded list
6147    blocks the parser for the duration of the dump and can consume
6148    megabytes on constrained clients. the same problem motivated
6149    pagination on member.list and COMM op=list. offset= / limit= with
6150    total= in the OK bounds the worst case at 500 per page while being
6151    generous for any real community. the proactive push on SUB (%-section_10_4-%) handles the common case of warming the emoji cache; pagination
6152    is for management UIs and large-community clients needing the full
6153    set explicitly.
6154
6155  why a line-oriented voice relay instead of WebRTC?
6156    WebRTC requires SDP parsing, ICE, STUN/TURN negotiation, and DTLS-SRTP.
6157    implementing this is a substantial undertaking even in Go, and is orders of
6158    magnitude more complex than a TLS socket. the WIRE voice relay design uses a
6159    second TCP/TLS connection carrying VFRAME lines with base64-encoded audio
6160    chunks. the control plane (join, leave, who is talking) stays on the main
6161    WIRE connection. pcm16le requires no codec library --- just decode base64 and
6162    write PCM bytes to the audio device. Opus is supported for quality-sensitive
6163    clients; libopus has Go bindings. the server acts as a relay mixer, not a
6164    P2P signaling hub; this is the Mumble model, which is simpler, more
6165    firewall-friendly, and implementable in a few hundred lines of Go.
6166
6167  why SRVADM is a separate verb from COMM?
6168    COMM ops are community-scoped: they carry gid= and operate on
6169    community state. SRVADM ops are server-scoped: they operate on
6170    server-level permissions and the server.admin role. mixing them
6171    under COMM would require a special sentinel gid value (e.g.
6172    gid=server) and complicate the permission check on every COMM op.
6173    a distinct verb makes the scope unambiguous and keeps the
6174    server.admin permission check out of the community permission
6175    resolution algorithm.
6176
6177  why challenges are out-of-band?
6178    in-protocol challenges (CAPTCHA, email verification, quizzes) require
6179    the server to implement a response-evaluation state machine, the client
6180    to implement a challenge-type-specific UI, and the spec to enumerate or
6181    extensibly define challenge types. none of this belongs on the wire.
6182    the challenge is fundamentally a human interaction with the server
6183    operator's policy; the wire protocol's job is to signal that one is
6184    required (ERR code=CHALLENGE url=<url>) and then accept the result
6185    (COMM op=create succeeds after the uid is granted community.create
6186    out-of-band). L0 minimal clients display the URL. GUI clients open it
6187    in a browser. the server integrates with any existing identity system.
6188    the protocol stays clean and does not couple itself to any specific
6189    verification mechanism.
6190
6191  why PING op=dismiss instead of relying on READSTATE to clear notifications?
6192    READSTATE op=mark advances the read cursor but is a different semantic:
6193    a user may want to dismiss a notification (stop being interrupted) without
6194    marking the underlying message as read. the two operations are orthogonal.
6195    PING op=dismiss also explicitly syncs notification state across sessions of
6196    the same uid via a broadcast to other sessions --- READSTATE op=mark already
6197    does this for read state, and PING op=dismiss follows the exact same pattern.
6198    without op=dismiss, a notification cleared on one device silently persists
6199    on all others until the next reconnect delivers the same PING again.
6200
6201  why READSTATE op=query mid= (single-message query)?
6202    READSTATE op=mark mid= marks a single message seen. without a matching
6203    op=query mid=, there is no way to confirm the server recorded it, or to
6204    check the status of a specific message without fetching the entire channel's
6205    read state. the single-message query completes the mark/query symmetry that
6206    exists at every other granularity: one cid and one gid already had query
6207    forms; mid= was the only missing level. both the mark and query forms
6208    require cid= because mid is only unique within a cid (%-section_3-%); the server
6209    stores seen state keyed on (cid, mid). this contrasts with PING state,
6210    which is keyed on (uid, mid) alone for reasons explained in %-section_6_8-%.
6211
6212  why SUB op=list?
6213    subscriptions are server-side state that the client depends on to understand
6214    what events it will receive. without a query form, subscriptions are the
6215    only hidden state in the protocol --- violating the "transparent, no hidden
6216    state" design goal. SUB op=list costs one line of server implementation and
6217    closes that gap entirely.
6218
6219  why does SUB op=list need pagination?
6220    every other list in the protocol is paginated. an unpaginated SUB op=list
6221    means a uid subscribed to many channels produces an unbounded reply that
6222    blocks the parser for the duration of the dump. the pagination parameters
6223    and defaults are identical to every other list: offset=0, limit=50, max 200.
6224
6225  why subscriptions are durable (uid-scoped, not connection-scoped)?
6226    subscriptions express which channels a user wants to receive broadcasts
6227    from. this is a property of the user, not of a terminal or connection.
6228    the same user on phone and desktop wants the same channels --- they are
6229    one identity. treating subscriptions as ephemeral per-connection state
6230    was an inconsistency: every other uid-scoped state (community membership,
6231    read cursors, PING queue, roles) is server-persisted and survives
6232    reconnection. subscriptions were the sole exception, which forced every
6233    client to maintain local storage solely to remember which channels to
6234    re-subscribe to after a disconnect. making subscriptions durable closes
6235    that gap: authentication alone is sufficient to fully resume. the server
6236    GC rules (clear subscriptions on leave, kick, ban, channel delete,
6237    community delete) keep the stored set consistent with membership reality
6238    without client involvement.
6239
6240  why UNSUB is now necessary as a first-class verb?
6241    when subscriptions were ephemeral, "unsubscribing" was implicit:
6242    the connection closed and the subscription vanished. with durable
6243    subscriptions, a client that wants to stop receiving broadcasts from
6244    a channel must explicitly remove it from the uid's stored set. UNSUB
6245    cid= is the symmetric counterpart to SUB cid=, following the same
6246    pattern as REACT/UNREACT, PIN/UNPIN, and mark/unmark idioms throughout
6247    the protocol.
6248
6249  why are multi-session SUB/UNSUB uid-scoped rather than session-scoped?
6250    the subscription set belongs to the uid, not the connection. a user
6251    who subscribes to #general from their phone is expressing intent for
6252    their identity, not their device. all sessions of that uid share one
6253    set and all receive broadcasts. notification suppression (muting) is
6254    stored server-side via KVAL scope=mute (%-section_7_5b-%) for multi-device
6255    sync; the client interprets mute preferences by suppressing notification
6256    sounds and desktop alerts. the protocol models intent and persistence;
6257    the client models presentation.
6258
6259  why COMM op=invite.info instead of restoring COMM op=invite.use?
6260    invite.use tried to solve gid-resolution by making the server accept
6261    a code-only join and derive the gid internally, producing a different
6262    OK verb in the process --- breaking the claimed identity with op=join.
6263    the correct solution is to separate the two concerns: resolving a code
6264    is a read operation (query -> metadata); joining is a write operation
6265    (mutate -> membership). COMM op=invite.info is the read half, following
6266    the same pattern as every other single-item info query in the protocol
6267    (COMM op=channel.info, COMM op=member.info, THREAD op=info, etc.).
6268    it requires no permission, returns community metadata including gid,
6269    and lets the client show a join-preview UI before committing. the join
6270    itself then uses the canonical COMM op=join gid= code= with a known gid.
6271
6272  why remove COMM op=invite.use?
6273    it was declared "identical" to COMM op=join gid= code= but had a different
6274    OK verb (verb=COMM op=invite.use vs verb=COMM op=join). "must behave
6275    identically" is false when the server emits different confirmations. the
6276    gid-resolution problem it tried to solve is now handled correctly and
6277    orthogonally by COMM op=invite.info.
6278
6279  why COMM op=channel.list and COMM op=role.list now have pagination?
6280    every other list operation (member.list, invite.list, COMM op=list,
6281    emoji.list, SRVADM op=list) is paginated with offset=, limit=, and total=.
6282    channel.list and role.list were the only exceptions, which means a community
6283    with many channels or roles would produce an unbounded reply that blocks the
6284    parser. the fix is the same pagination pattern applied everywhere else.
6285
6286  why COMM op=member.info uid=?
6287    every other entity supports both a list query and a single-item query:
6288    COMM op=channel.info cid=, COMM op=info gid=, THREAD op=info tid=, WHO uid=.
6289    members were the only entity where list existed but single-item lookup did
6290    not. a client that wants to display one member's roles and join date should
6291    not have to paginate through the full member list to find them.
6292    creation and administration of invite codes are different trust levels.
6293    a community may want moderators to be able to invite friends (invite.create)
6294    without giving them oversight of all outstanding codes (invite.manage).
6295    separating them follows the same deny-wins permission model: invite.manage
6296    can be withheld from invite.create holders without touching the role
6297    definition, just by not granting it (per %-section_8_2-% super-permission rule).
6298
6299  why mention.everyone and mention.here are hard rejects (ERR) while
6300  mention.role is a soft drop (WARN)?
6301    fan-out for 'everyone' or 'here' can be community-wide; the server
6302    cannot silently discard such a request the way it can drop a
6303    user-defined role mention. the cost must be rejected at the source
6304    before fan-out is attempted. user-defined role mentions can be
6305    quietly dropped because the damage is bounded; built-in role mentions
6306    are not. the three tokens (mention.role, mention.everyone, mention.here)
6307    are all in the defined permset so clients can grant or deny them
6308    individually like any other permission.
6309
6310  why SYNC instead of relying solely on READSTATE broadcast?
6311    (this rationale is superseded. SYNC was removed in 0.8; see
6312    "why READSTATE cursor= instead of a separate SYNC verb?" above.)
6313
6314  why COMM op=role.perm instead of delete+recreate to change permissions?
6315    every other mutable field on a role has an update operation: rename
6316    changes name=; appearance changes color=, emoji=, hoist=. permissions
6317    (perms=) were the only field settable at role.create time with no
6318    subsequent update path. requiring delete+recreate to change permissions
6319    on a live role breaks all existing role assignments (rolids are
6320    server-assigned ULIDs; the new role gets a new rolid) and forces all
6321    clients to invalidate their role cache. COMM op=role.perm updates
6322    permissions in-place, preserving the rolid and all assignments.
6323
6324  why role.perm and channel.perm have different "clear by omission" semantics?
6325    the two operations have structurally different data models:
6326
6327      COMM op=role.perm operates on a flat permission set (one dimension:
6328      a token is either in the set or not). absent perms= = clear all
6329      because a flat set has only one state to clear.
6330
6331      COMM op=channel.perm operates on a two-axis sparse override matrix:
6332      allow= and deny= are independent. a role may have only an allow
6333      override, only a deny override, or both. absent allow= means "no
6334      change to (or clear of) the allow axis"; absent deny= means "no
6335      change to (or clear of) the deny axis". to clear both axes, omit
6336      both tags. this per-axis clearing is not achievable with a single
6337      flat "clear all" sentinel without introducing a magic token value,
6338      which would violate the 1*TCHAR constraint (%-section_2_1-%).
6339
6340    the asymmetry is structural and correct. it is a consequence of the
6341    underlying data models, not an inconsistency. implementors MUST NOT
6342    apply the role.perm clearing rule to channel.perm or vice versa.
6343
6344  why ROLE op=info includes perms=?
6345    every other entity list returns the full entity field set. ROLE op=info
6346    was the sole list response that omitted a key field: perms=. the ROLE
6347    op=create broadcast carried perms=; the query did not. a fresh-connecting
6348    client doing COMM op=role.list could learn role names and visual appearance
6349    but not what any role is permitted to do --- forcing it to rely on its
6350    create-event cache, which is unavailable on first connect. adding perms=
6351    to op=info makes the query response self-contained and consistent with
6352    every other entity query in the protocol.
6353
6354  why ROLE op=create broadcast now carries the full entity field set?
6355    ROLE op=info defines the canonical field set for a role entity (name,
6356    color, emoji, hoist, perms). every live ROLE event that describes a role
6357    state change should carry the same complete picture, so a client
6358    receiving op=create can fully cache the role without a follow-up query.
6359    the create broadcast previously omitted emoji= and hoist=. the info and
6360    create formats are now identical in field set, matching the pattern used
6361    by CHAN op=create vs CHAN op=info.
6362
6363  why the proactive role push uses ROLE op=info instead of op=appearance?
6364    the proactive push on SUB is intended to warm the client's role cache so
6365    it can render mentions and member lists without a separate COMM op=role.list
6366    round-trip. ROLE op=appearance carries only the visual subset (color, emoji,
6367    hoist). a client that receives appearance-only pushes has display data but
6368    not role names or permissions --- it cannot answer "can this uid send messages
6369    here?" without a follow-up. ROLE op=info lines carry the full entity.
6370    this is the same reason the emoji push uses EMOJI op=info lines (%-section_10_4-%) rather than EMOJI op=add events: the push should give the client
6371    a complete, self-sufficient cache entry.
6372
6373  why KVAL op=get instead of a dedicated query verb per scope?
6374    every server-persisted key-value store needs a read path. the
6375    alternative is a scope-specific query verb: COMM op=brand.get for
6376    communities, UPROFILE op=get for users. but the query contract is
6377    identical in both cases: single-key returns one op=info line; all-keys
6378    returns N op=info lines; NOTFOUND on missing single key. KVAL op=get
6379    with scope= selected by tag expresses the same query without forking
6380    the verb surface. parsers handle the response with one handler
6381    regardless of which scope was queried.
6382
6383  why KVAL op=del instead of empty-payload op=set for key deletion?
6384    the empty-payload convention (line ends with ' :' immediately before
6385    LF) is a side-channel that overloads the set operation with delete
6386    semantics based on payload length. this means the server must inspect
6387    the payload to decide whether to write or delete --- two distinct
6388    mutations with different broadcast shapes (op=info for a set,
6389    op=del for a deletion) sharing one op= value. op=del is an explicit,
6390    dedicated operation: unambiguous to parse, distinct in the broadcast,
6391    and idempotent by spec (deleting a non-existent key is a silent OK
6392    with no broadcast). parsers never need to inspect payload length to
6393    decide what happened.
6394
6395  why COMM op=update instead of KVAL scope=comm key=name for the display name?
6396    KVAL scope=comm key=name is explicitly rejected (ERR BADARG) to
6397    prevent a second write path for the community display name. COMM
6398    op=update name= is the single source of truth: the same field set
6399    on op=create is updated by op=update and returned by op=info and
6400    op=list. allowing KVAL to set it would create two stores for the
6401    same field with no clear authority. the same op covers open= because
6402    that flag is part of the same community record and would otherwise
6403    be equally immutable after creation.
6404
6405  why COMM op=channel.rename?
6406    channel display names were immutable after creation; delete+recreate
6407    was the only path. this broke the pattern established by COMM op=role.rename:
6408    every other named entity (community via op=update, role via op=rename)
6409    supports a display name update. the channel slug is kept stable by design
6410    (rid paths must be bookmarkable), so rename is always display-name-only.
6411    the same permission gate (channel.manage) and broadcast pattern (CHAN
6412    op=rename to all members) as op=topic and op=perm.
6413
6414  why UPLOAD op=status?
6415    the UPLOADED event is asynchronous: the server sends it after the HTTP
6416    PUT completes. this is the correct model for GUI clients, which manage
6417    an HTTP client and a WIRE connection independently. CLI/TUI/ACME clients
6418    typically use a shell script or simple program that issues the WIRE
6419    negotiation, shells out to curl or hget for the PUT, then returns to the
6420    WIRE connection. in this flow, the client has no receive loop active
6421    during the HTTP PUT; it will never see the UPLOADED event. UPLOAD op=status
6422    is the synchronous poll alternative: after the PUT completes, the client
6423    issues UPLOAD op=status aid= on the WIRE connection and receives a
6424    deterministic state reply. the design mirrors READSTATE op=query: the
6425    event (UPLOADED) and the query (op=status) operate on the same server-side
6426    state; one is push, the other is pull. both paths are needed for full
6427    client diversity.
6428
6429  why are community links in the description markup document instead of structured keys?
6430    the previous revision used a JSON array (rejected: requires a JSON parser
6431    in all clients) and then structured key=value entries (link.N.label,
6432    link.N.url). the structured approach was also rejected: it has no deletion
6433    path (empty values violate %-section_2_1-% 1*TCHAR), no reorder operation, and
6434    requires N-index management for what is fundamentally an authored list.
6435    community links are human-authored content, not machine-structured data.
6436    wire markup's [label](url) inline syntax is the protocol's designated
6437    representation for hyperlinks in readable content (%-section_9_2-%). embedding
6438    links in the description document requires no new grammar, no new key names,
6439    no deletion operation (rewrite the document), and no ordering operation
6440    (document order). the description document is already the community's public
6441    face; link bars naturally live there. the five remaining brandkeys
6442    (description, icon, banner, color, rules) are all genuinely scalar machine-
6443    consumed fields. no scalar brandkey has a deletion or ordering problem.
6444
6445  why do all single-item queries now have terminal OKs?
6446    multi-item queries (op=list forms) have always had terminal OKs to delimit
6447    the response stream. single-item queries returned exactly one line and were
6448    considered self-delimiting. but a parser cannot know a response is complete
6449    until it has received all of it; for single-item queries this means the
6450    parser must recognise the specific response verb and consider it terminal.
6451    this special-cases every single-item query in the parser. a uniform rule ---
6452    every query ends with OK --- means the parser's frame-completion logic is the
6453    same for all query types. it also makes the query surface consistent with
6454    the rest of the spec where every mutating operation already produces an OK.
6455    the cost is one extra line per query; the benefit is a simpler, more
6456    testable parser: frame-completion logic is identical for all query types.
6457    clients reading line-by-line (L0 minimal clients, bots, ACME editors)
6458    benefit especially from not needing to special-case per-verb terminators.
6459
6460  why is PING op=list now paginated?
6461    every other list in the protocol is paginated. an unpaginated PING op=list
6462    exposes a specific DoS vector: a user offline for weeks receives an unbounded
6463    burst on reconnect and again if he calls PING op=list explicitly. for a
6464    CLI/TUI/ACME client running in a 24-line terminal or a shell pipe, an
6465    unbounded dump blocks the read loop for an unspecified duration. the 50/200
6466    defaults and the total= field in the OK match every other paginated list in
6467    the spec; no new pattern is introduced.
6468
6469  why does UNSUB require cid= (and reject without it)?
6470    SUB already rejects without rid= or cid=; UNSUB had no matching rule. an
6471    UNSUB with no arguments is structurally ambiguous --- it cannot mean "unsubscribe
6472    from everything" because that would conflict with the GC rules (which handle
6473    mass-unsubscribe on leave, kick, ban, delete). the symmetric rejection keeps
6474    UNSUB's argument requirement identical to SUB's, and an explicit BADARG is
6475    better than silently accepting a no-op or producing undefined behaviour.
6476
6477  why can EDIT on board channels update tags= without a body?
6478    tags on a board post are a categorisation signal, not message content. a
6479    moderator recategorising a post should not need to re-transmit the full post
6480    body to update its tags. requiring body alongside tags= is wasteful (the
6481    body may be kilobytes of markup) and potentially lossy if the client's cache
6482    is stale. the orthogonal rule --- body and tags= are independently optional
6483    in EDIT on board channels --- is consistent with how other tag-bearing fields
6484    work in the protocol: only the fields that change are sent.
6485
6486  why exactly one owner rather than at least one?
6487    "at least one owner" permits two simultaneous owners and makes the transfer
6488    semantics ambiguous: if uid A transfers to uid B while uid C also holds
6489    owner (from a prior grant), nothing changes meaningfully. the community
6490    owner concept is a trust anchor, not a role: it must be uniquely identifiable
6491    for moderation escalation and community deletion. exactly-one makes the
6492    invariant enforceable by the server in a single atomic swap. the transfer
6493    becomes a pure assignment: B gets owner, A loses it, atomically, with no
6494    intermediate state. server implementations have a simple test for the
6495    invariant: count of owner holders is exactly 1 before and after every
6496    owner-related operation.
6497
6498  why does THREAD op=info carry author= in addition to opener=?
6499    opener= identifies the opening message mid. deriving the author requires a
6500    message lookup (fetch the MSG at opener= and read its uid=). for text-channel
6501    threads created implicitly, this lookup is the only path to the author uid
6502    unless the server stores it separately. lock and unlock permission checks
6503    run on every MSG send to a locked thread and on every THREAD op=lock/unlock
6504    request; doing a message lookup in a hot path is avoidable. storing author=
6505    on the thread record at creation time --- when the MSG is already in hand ---
6506    costs one field. all subsequent permission checks are O(1) against the
6507    thread record. the same pattern is used for locked_by= (already stored) and
6508    for ts= (stored at last event).
6509
6510  why is pcm16le constrained to mono?
6511    stereo pcm16le at 48kHz 16-bit produces 3840 bytes per 20ms frame. base64
6512    encoding at 4/3x overhead yields 5120 bytes, which exceeds the 4096-byte
6513    line limit (%-section_1-%). opus is immune: stereo opus frames at 20ms are
6514    typically 40-160 bytes encoded (~215 bytes base64), safely within the limit.
6515    pcm16le's value is as a zero-dependency fallback: a client needs only a
6516    base64 decoder and a PCM audio output --- both trivial in Go. stereo pcm16le
6517    has no plausible use case that opus does not serve better; constraining it
6518    to mono eliminates a latent line-limit violation and removes any temptation
6519    to define stereo pcm16le in a future revision.
6520
6521  why document the MEMBER op=delete delivery gap explicitly?
6522    the delivery rule (MEMBER events go only to subscribed clients) is stated
6523    in %-section_8_0b-% but its consequence for the community-destruction event was
6524    not. a member who has unsubscribed from all channels has no live path to
6525    receive MEMBER op=delete and no way to know the community was deleted until
6526    his next active query. making the fallback (ERR code=NOTFOUND on COMM op=info
6527    triggers purge) normative closes the gap: the client has a deterministic
6528    recovery path regardless of whether it received the broadcast.
6529
6530  why add a normative CAPS disambiguation rule?
6531    every dual-direction verb in the protocol has a normative MUST stating how
6532    parsers distinguish client-sent from server-sent lines. ATTACH uses
6533    tag-presence as the discriminant (%-section_12_3-%). EMOJI and KVAL use op= value
6534    (%-section_10_3-%, %-section_7_5-%). CAPS was the only dual-direction verb without an explicit
6535    MUST. the rule (list= present = server reply; absent = client query) is
6536    obvious from context but should be testable without tracking protocol
6537    state. making it a MUST is zero-cost and closes the last gap in the
6538    dual-direction verb documentation.
6539
6540  why TYPING and PRESENCE events are fire-and-forget (no OK)?
6541    typing and presence are ephemeral status signals, not mutations of durable
6542    server state. an OK reply would arrive after the signal is already stale:
6543    typing stops after 3 seconds of inactivity; presence transitions happen
6544    multiple times per session. making the client wait for confirmation before
6545    considering each state change sent would add a round-trip to every keystroke
6546    indicator and every status toggle. both verbs are closer in nature to UDP
6547    datagrams than to RPC calls. the server MUST NOT reply to event lines;
6548    clients MUST NOT wait. ERR is still valid for malformed lines (ERR
6549    code=BADARG); only valid event lines receive no reply. the op=list query
6550    forms (TYPING op=list, PRESENCE op=list) are not events and do receive
6551    OK replies; they are not fire-and-forget.
6552
6553  why DM channels are created implicitly on first SUB?
6554    an explicit COMM op=dm.create verb would require the client to learn the
6555    cid before sending the first message, adding a round-trip. SUB rid=@<uid>
6556    is already the correct expression of intent ("I want to talk to this person");
6557    having the server create the DM atomically on first SUB closes the loop with
6558    zero extra protocol surface. the symmetric cid guarantee means both sides
6559    reach the same channel regardless of who subscribes first. this mirrors how
6560    IRC private messaging works conceptually while keeping the server as the
6561    authority on cid assignment.
6562
6563  why HIST op=purge instead of a DELCHAT or WIPE verb?
6564    a dedicated DELCHAT or WIPE verb would be DM-only and would need to
6565    address channel-object deletion (breaking cid permanence), cascading
6566    reference invalidation (mid, tid, aid, READSTATE, PING), and
6567    pagination cursor breakage. HIST is the existing read/replay surface
6568    for the event log; op=purge is the symmetric write (erase) surface.
6569    using the HIST verb keeps the scope unambiguous: purge operates on
6570    the event log, not on the channel object. the channel object, its
6571    cid, its subscriptions, and its future messages are entirely
6572    unaffected. this is orthogonal with the existing HIST op surface
6573    and consistent with the COMM op=delete pattern (community deleted;
6574    its content erased; references retired). before= makes the same
6575    primitive serve both "wipe everything" and "time-bounded retention"
6576    without a second verb.
6577
6578  why delete_policy is immutable after DM creation?
6579    a participant who agreed to mutual consent before sharing sensitive
6580    information must be able to rely on that agreement for the life of
6581    the channel. if policy were mutable, a bad actor could: open a DM,
6582    negotiate mutual consent, share damaging information about the other
6583    party, then flip to unilateral and purge the evidence before the
6584    other party can act. immutability makes the policy a verifiable
6585    contract: clients can query the policy (CHAN op=info carries it;
6586    %-section_8_4-%) and display a trust indicator to users. the cost of
6587    immutability is low --- users who want different terms must start a
6588    new DM channel.
6589
6590  why E2E uses X25519 separately from Ed25519?
6591    See %-section_29_2f-% for the authoritative rationale.
6592
6593  why X3DH and Double Ratchet are RECOMMENDED but not normative?
6594    See %-section_29_2f-% for the authoritative rationale on algorithm agility,
6595    interoperability, and privacy implications.
6596
6597  why PUBKEY op=prekey.fetch consumes an OPK atomically?
6598    See %-section_29_2f-% for the authoritative rationale on single-use guarantees
6599    and graceful degradation.
6600
6601  why PIN/UNPIN need a normative uid= discriminant?
6602    every dual-direction verb in the spec that carries different tags in each
6603    direction has a normative MUST rule for parser disambiguation. EDIT uses
6604    uid= presence (%-section_6_4-%); DEL uses uid= presence (%-section_6_5-%); KVAL uses op= value
6605    (%-section_7_5-%); ATTACH uses tag presence (%-section_12_3-%). PIN and UNPIN follow
6606    the same pattern: client-sent lines carry only mid=; server-broadcast lines
6607    carry uid=, cid=, and ts=. without a stated normative rule, a parser has no
6608    contract to test against. the rule is trivially derivable from the formats
6609    but must be stated as a MUST to be normative.
6610
6611  why HIST has_more= is not derivable from count alone?
6612    a client might assume has_more = (count == limit). this fails for
6613    reaction-dense windows: a window of limit=50 messages may return 2 MSG
6614    lines and 98 REACT/UNREACT lines for a total count of 100 event lines,
6615    none of which tells the client whether more messages exist beyond the window.
6616    has_more is a single boolean emitted by the server after evaluating its
6617    event log; it is the only correct answer to "should I paginate further?"
6618
6619  why UPLOAD op=status echoes purpose=/cid=/gid=?
6620    a client managing multiple concurrent uploads cannot route a bare
6621    UPLOAD op=status aid= reply to the correct context without out-of-band
6622    state. purpose= tells the client what kind of asset this is; cid= or gid=
6623    tells it which channel or community it belongs to. echoing routing context
6624    is consistent with every other query reply in the protocol: COMM op=channel.info
6625    echoes cid=; THREAD op=info echoes cid= and tid=; COMM op=member.info echoes
6626    gid= and uid=. UPLOAD op=status should not be the lone exception.
6627
6628  why purpose=avatar is a distinct upload purpose?
6629    avatar assets are not attached to any message (no cid=) and not registered
6630    with any community (no gid=). they are uid-scoped identity assets. without
6631    a distinct purpose=avatar, a client uploading an avatar either has no valid
6632    cid= to provide (rejected by purpose=message rules) or must use a sentinel
6633    gid= (meaningless for a server-wide identity asset). a separate purpose value
6634    allows the server to apply avatar-specific size and dimension policies and
6635    makes the intent explicit at upload time.
6636
6637  why TYPING op=list despite state expiring within seconds?
6638    the "no hidden state" design goal (%-section_2-%, %-section_23-%) holds that every ephemeral
6639    server state with a write path MUST have a read path. TYPING state=start
6640    is a write. before op=list existed, the server silently accumulated
6641    per-cid typing maps with no query surface. a client reconnecting mid-
6642    typing had no way to learn who was typing. the result is a query whose
6643    answer is stale the moment it arrives --- but this is also true of WHO
6644    (a user can disconnect between the WHO request and the reply) and we
6645    accept it for PRESENCE op=list by the same reasoning. the best-effort
6646    caveat applies; the alternative (no query) violates the design goal
6647    permanently. the server-side cost is a memory map identical in structure
6648    to PresenceStore; it already existed implicitly.
6649
6650  why PRESENCE op=list?
6651    WHO (%-section_7_1-%) provides per-uid presence read access: the PROFILE reply
6652    includes status= from the server's presence store. this satisfies the
6653    "no hidden state" invariant's letter (every write path has a read path)
6654    but not its spirit: a client connecting to a community of N members has
6655    no efficient way to bootstrap presence state. issuing WHO for each member
6656    is O(N) round-trips. PRESENCE op=list provides the same snapshot
6657    semantics as TYPING op=list: a best-effort bulk query scoped to a
6658    community (gid=). the result is stale the moment it arrives --- the same
6659    caveat accepted for WHO and TYPING op=list. invisible members are omitted
6660    (they appear offline to all other uids, consistent with the broadcast
6661    rule). the spec-correct default for a user with no presence entry is
6662    offline (%-section_7_2-%); op=list gives clients the snapshot needed to
6663    initialize presence accurately on connect. the server-side cost is zero:
6664    PresenceStore already exists.
6665
6666  why COMM op=member.list has no permission gate?
6667    the previous gate (community.admin or moderation permission) was
6668    inconsistent: COMM op=member.info (single-uid lookup) already returned
6669    identical fields --- including banned_until= and timeout_until= --- to any
6670    community member. the list form exposed no data that per-uid queries
6671    did not. the gate was protecting against bulk enumeration, which is a
6672    rate-limit concern (%-section_18-%), not a permission concern. an inconsistent
6673    gate that does not protect against a real threat while breaking L3
6674    ergonomics (Discord-style member sidebars require the list) is worse
6675    than no gate. removed.
6676
6677  why EDIT, REACT, and UNREACT server broadcasts carry cid=?
6678    %-section_2_5-% states "multi-line response verbs carry sufficient identifying
6679    tags in each response line." before this change, EDIT, REACT, and
6680    UNREACT in HIST had no cid= --- they were context-dependent on the
6681    pending HIST request. this made pipelined HIST for multiple channels
6682    require purely pending-request routing, with no fallback. adding cid=
6683    costs a 26-char ULID per event line (within all framing limits) and
6684    makes every event line in the protocol self-identifying, consistent
6685    with PIN and UNPIN which already carried cid=. the client-sent forms
6686    are unchanged.
6687
6688  why PIN op=list instead of reconstructing from HIST?
6689    replaying full HIST to reconstruct pin state is O(channel history)
6690    for a property (current pinned set) that is O(small). Discord exposes
6691    /channels/{id}/pins for exactly this reason. the pin index is already
6692    implicitly required by PIN idempotency (a server cannot return OK
6693    silently on a double-PIN without tracking current pin state). PIN
6694    op=list surfaces an index the server already maintains.
6695
6696  why INVITE op=info routing uses %-section_2_5-% only (no creator= discriminant)?
6697    the previous discriminant (creator= present = list reply, absent =
6698    info reply) was fragile: a buggy server emitting invite.list without
6699    creator= was indistinguishable from a correct invite.info reply.
6700    %-section_2_5-% pending-request state already provides unambiguous routing --- that
6701    is its purpose. tag-presence discriminants are secondary checks that
6702    add complexity without adding reliability when the primary mechanism
6703    is already sound. the permission-derived field set (creator= and ts=
6704    present when caller has invite.manage) makes the two forms naturally
6705    differ in optional tags, not in mandatory ones.
6706
6707  why MENTION_ROLE uses three distinct WARN codes?
6708    the original MENTION_ROLE_TRUNCATED covered all three cases: noperm,
6709    invalid rolid, and threshold exceeded. these are distinct server
6710    actions with different client remediation paths. NOPERM means the
6711    sender lacks the permission and should not retry. INVALID means one
6712    or more rolids do not exist in the community; the client should fix
6713    the rolmentions= list. TRUNCATED means the list was too long and was
6714    cut; the client could retry with fewer entries. collapsing all three
6715    into one code forced clients to parse the human-readable reason payload
6716    to distinguish them --- a violation of the invariant that clients MUST
6717    use code= for control flow and MUST NOT use the reason for programmatic
6718    logic (%-section_17-%). three codes, one handler per case.
6719
6720  why MEMBER op=info carries ts=?
6721    MEMBER op=info is an entity query reply. per %-section_2_6-%, all op=info
6722    replies carry ts= as the unix-ms of the most recent mutation.
6723    member record mutations include role assignment/revoke, ban, timeout,
6724    unban, and untimeout. a client rebuilding state after a period offline
6725    needs to know whether the record it is caching is current; ts= is the
6726    cheapest way to provide that signal. joined= is the creation timestamp
6727    and answers a different question. both fields are needed: joined= for
6728    \"how long has this member been here?\", ts= for \"has anything changed
6729    since I last saw this record?\".
6730
6731  why ROLE op=info carries ts=?
6732    same rationale as MEMBER op=info: ROLE op=info is an entity query
6733    reply subject to the %-section_2_6-% ts= invariant. role mutations include
6734    rename, perm change, and appearance change. ROLE op=create already
6735    carried ts=; omitting it from op=info was an oversight. the pre-SUB
6736    role cache warm push uses op=info lines; clients that compare ts=
6737    values to detect stale cache entries need ts= present in the format
6738    they receive, not only in the create/rename/perm broadcast events.
6739
6740  why EMOJI op=info carries ts=?
6741    same rationale: EMOJI op=info is an entity query reply subject to the
6742    %-section_2_6-% ts= invariant. emoji mutations include rename. the cache-warm
6743    push on SUB uses EMOJI op=info lines; ts= lets clients validate
6744    cache freshness. omitting ts= from op=info while carrying it on
6745    op=add/rename events was an oversight.
6746
6747  why KVAL instead of per-entity ad-hoc key-value verbs (BRAND, UPROFILE)?
6748    both community branding and user profile decoration are key-value
6749    stores: a bounded set of named fields, each independently settable,
6750    gettable, and deletable, with a uniform broadcast on write. the
6751    original design gave each a different verb (BRAND for communities,
6752    UPROFILE for users), duplicating the same op= surface, broadcast
6753    shape, NOTFOUND semantics, and key-limit rules under two names.
6754    KVAL unifies them under one verb with a mandatory scope= tag that
6755    selects the store. the verb is a pure primitive: op= selects the
6756    operation, scope= selects the store, id= identifies the entity,
6757    key= names the field. no field of KVAL changes meaning depending on
6758    another field. parsers dispatch on verb then op= only. adding a new
6759    scope in a future revision (e.g. scope=server for server-level
6760    metadata) requires a new registry entry and no verb changes.
6761    the cost is that KVAL is less self-documenting than a named verb:
6762    a reader must consult the scope registry (%-section_7_5b-%) to know which keys
6763    are valid. this is the correct trade-off for a primitive: scope
6764    documentation belongs in the registry, not in the verb name.
6765
6766%- future_optional_extensions = "Sect.  25" -%
6767========================================================================
6768%-future_optional_extensions-% -- FUTURE / OPTIONAL EXTENSIONS
6769========================================================================
6770
6771  POLL    -- community polls on messages or boards
6772  SCHED   -- scheduled events with RSVP
6773  AUDIT   -- structured mod-log event stream (replaces implicit logging).
6774             in 0.8, ban, kick, and timeout reasons are stored server-side
6775             but have no protocol query surface. AUDIT will provide a
6776             paginated query verb for stored moderation events.
6777  SEARCH  -- server-side full-text search over message history
6778  HISTREV -- message edit history retrieval (HIST rev= parameter);
6779             the rev= counter on EDIT events is already normative; this
6780             extension would expose prior revisions via HIST. servers that
6781             retain edit history SHOULD reserve storage for this.
6782  KEYSTATE -- query the expiry date of an old-uid alias created by
6783              KEYROTATE (%-section_4_6-%). in 0.8 there is no protocol
6784              path for a user to confirm whether his old-uid grace period
6785              is still active or has expired.
6786  CHAN op=order -- user-controlled channel ordering (drag-and-drop
6787              reorder within a community). in 0.8 COMM op=channel.list
6788              returns channels in ULID (creation) order, which is the
6789              canonical and only defined order. this extension would add
6790              CHAN op=order gid=<gid> order=<csv-cid> as a management
6791              command (requires channel.manage or community.admin) that
6792              sets an explicit position array, broadcast as a CHAN op=order
6793              event to all subscribers. clients that receive a CHAN op=order
6794              event MUST use the supplied order array instead of cid order
6795              for display. the extension does not alter COMM op=channel.list
6796              return order; it adds a separate display-order store.
6797  from= tag -- reserved for disambiguation of bidirectional verbs where
6798              uid= identifies different principals in each direction
6799              (%-section_29_2c-%). trigger condition: if a second verb joins
6800              PUBKEY op=keysync in requiring this asymmetry, from= MUST
6801              be adopted for all such verbs simultaneously. a from=
6802              extension would carry the originating uid explicitly,
6803              making direction unambiguous without session-context tracking.
6804
6805  extensions gate on CAPS. unknown verbs always return ERR UNKNOWN.
6806  no extension may alter the semantics of any core verb.
6807
6808  purge and e2e are defined in %-section_29-% and are gated on cap=purge
6809  and cap=e2e respectively. they are not listed as future stubs here
6810  because they are fully normative in 0.8.
6811
6812%- adversarial_behavior = "Sect.  26" -%
6813========================================================================
6814%-adversarial_behavior-% -- ADVERSARIAL BEHAVIOR AND PRESSURE MITIGATIONS
6815========================================================================
6816
6817  this section specifies normative server behavior against adversarial
6818  clients and high-load conditions. implementations ignoring this section
6819  are correct but operationally fragile.
6820
6821  26.0  WARN verb
6822
6823    when the server modifies a client's MSG (truncating mention lists,
6824    applying cooldowns, or dropping invalid entries) it MUST inform the
6825    sending client via a WARN line sent back on the same connection.
6826    WARN is distinct from ERR: the MSG was accepted and broadcast; WARN
6827    describes what the server changed before doing so.
6828
6829    WARN format:
6830
6831      WARN [mid=<mid>] code=<WARNCODE> :human-readable description
6832
6833    mid= identifies the MSG that was modified. mid= is present only when
6834    the WARN is associated with a specific message; advisory WARNs
6835    (OPK_DEPLETED, SKDM_PENDING) carry no mid= (see %-section_26_0-%, WARN code table).
6836    code is machine-readable; clients use it for UI decisions.
6837    payload is human-readable; clients MAY display it verbatim.
6838
6839    defined WARN codes:
6840
6841      MENTION_UID_TRUNCATED   -- uid entries in mentions= were dropped
6842                                 because the count exceeded
6843                                 MENTION_UID_DROP_THRESHOLD (%-section_26_1-%)
6844      MENTION_UID_INVALID     -- one or more uids were not community members
6845                                 and were dropped (%-section_26_2-%)
6846      MENTION_ROLE_NOPERM     -- entire rolmentions= tag dropped because
6847                                 the sender lacks mention.role permission
6848                                 (%-section_8_2-%)
6849      MENTION_ROLE_INVALID    -- one or more rolids were unknown in the
6850                                 community; those entries were dropped
6851                                 (%-section_6_2-%)
6852      MENTION_ROLE_TRUNCATED  -- rolid entries beyond
6853                                 MENTION_ROLE_DROP_THRESHOLD were dropped
6854                                 (%-section_26_1-%)
6855      MENTION_COOLDOWN        -- high-cardinality role cooldown active;
6856                                 PING fan-out suppressed for this MSG
6857      MENTION_COALESCED       -- PINGs to one or more targets were coalesced
6858                                 into a single delivery (see 26.4)
6859      ATTACH_STRIPPED         -- one or more aid= references were removed
6860                                 from the message body because the sender
6861                                 lacks attach.upload permission
6862      OPK_DEPLETED            -- this uid's one-time prekey pool is exhausted
6863                                 (%-section_29_2a-%). no mid= tag is present.
6864                                 the uid SHOULD upload new OPKs immediately
6865                                 via PUBKEY op=prekey.
6866      SKDM_PENDING            -- delivered to a sender on AUTH OK when the
6867                                 server has one or more stored PUBKEY
6868                                 op=keysync requests queued for that uid.
6869                                 the server delivers the stored keysync
6870                                 lines immediately after this WARN. the
6871                                 sender MUST issue the corresponding SKDMs.
6872                                 no mid= tag is present on this WARN.
6873
6874    WARN lines are sent only to the originating client, not broadcast.
6875    clients MUST NOT treat WARN as a protocol error.
6876    L0 clients that do not parse WARN lines are unaffected; they will
6877    simply not see the notification.
6878
6879  26.1  mention storms
6880
6881    a malicious or buggy client may include an unbounded number of uids
6882    or rolids in a single MSG, or mention a high-cardinality role
6883    repeatedly across many messages in rapid succession.
6884
6885    mitigations (normative):
6886
6887      mention processing order (normative):
6888        servers MUST process uid entries in mentions= in the following
6889        order; deviation will produce divergent WARN emission:
6890
6891          1. VALIDATE: remove all uid entries that are not members of
6892             the community (gid). emit WARN code=MENTION_UID_INVALID
6893             if any were removed.
6894          2. CAP: if the post-validation entry count exceeds the
6895             MENTION_UID_DROP_THRESHOLD, drop entries beyond the cap
6896             from the surviving (post-validation) set. emit WARN
6897             code=MENTION_UID_TRUNCATED if any were dropped.
6898
6899        the cap applies to the post-validation set. a sender is not
6900        penalized against the cap for entries that are invalid members;
6901        invalid entries are removed first, then the cap is applied to
6902        the remaining real mentions. if both conditions apply to a
6903        single MSG, both WARN lines MUST be sent.
6904
6905        the same validate-first ordering applies to rolid entries in
6906        rolmentions= (validate role membership / existence, then apply
6907        MENTION_ROLE_DROP_THRESHOLD cap).
6908
6909      MENTION_UID_DROP_THRESHOLD:
6910        servers MUST enforce a cap on the number of uid entries in
6911        mentions= per MSG. RECOMMENDED cap: 50.
6912        entries beyond the threshold are dropped.
6913        server MUST send WARN mid=<mid> code=MENTION_UID_TRUNCATED
6914        :truncated to N entries to the originating client.
6915
6916      MENTION_ROLE_DROP_THRESHOLD:
6917        servers MUST enforce a cap on the number of rolid entries in
6918        rolmentions= per MSG. RECOMMENDED cap: 10.
6919        entries beyond the threshold are dropped.
6920        server MUST send WARN mid=<mid> code=MENTION_ROLE_TRUNCATED
6921        :truncated to N entries to the originating client.
6922
6923      HIGH_CARDINALITY_COOLDOWN:
6924        applies to any role (user-defined or built-in) whose current
6925        membership cardinality exceeds a server-defined threshold
6926        (RECOMMENDED: 50 members). the cooldown is per (rolid, cid)
6927        pair, not per rolid alone: the same role can be mentioned in
6928        different channels at its natural rate.
6929
6930        after a successful high-cardinality role mention in a channel,
6931        subsequent mentions of the same role in the same channel within
6932        the cooldown window (RECOMMENDED: 60 seconds) have their PING
6933        fan-out suppressed. the MSG is still accepted and broadcast to
6934        channel subscribers.
6935
6936        server MUST send WARN mid=<mid> code=MENTION_COOLDOWN
6937        :@rolname ping suppressed; cooldown active until <unix-ms>
6938        to the originating client.
6939
6940        built-in roles 'everyone' and 'here' always satisfy the
6941        cardinality threshold (its cardinality is the full community
6942        or online subset). they are not special-cased; the same
6943        HIGH_CARDINALITY_COOLDOWN rule applies to them as to any other
6944        large role.
6945
6946      PING_FANOUT_ASYNC:
6947        servers MUST dispatch PING fan-out for roles whose cardinality
6948        exceeds the HIGH_CARDINALITY_COOLDOWN threshold out of the MSG
6949        ingestion hot path. the MSG broadcast to channel subscribers
6950        proceeds immediately; PINGs are queued and dispatched
6951        asynchronously. clients MUST tolerate PING arriving after MSG
6952        on a subscribed channel.
6953
6954  26.2  adversarial clients: invalid tag injection
6955
6956    a client may send mentions= containing uids that are valid ULIDs
6957    but not members of the community, attempting to trigger spurious
6958    PINGs for users in other communities or on other servers.
6959
6960    mitigation (normative):
6961      servers MUST validate every uid in mentions= is a current member
6962      of the community (gid) associated with the cid. invalid uids are
6963      dropped. the MSG proceeds with the remaining valid entries.
6964      server MUST send WARN mid=<mid> code=MENTION_UID_INVALID
6965      :N uid(s) dropped; not community members
6966      to the originating client if any were dropped.
6967      servers MUST NOT forward PINGs to users outside the community.
6968
6969  26.3  adversarial clients: PING replay / spoofing
6970
6971    a client cannot forge PING events directly (server generates them
6972    from MSG tags). however a client may attempt to cause the server to
6973    re-dispatch PINGs by re-sending a MSG with the same content.
6974
6975    mitigation: mid is server-assigned and unique. each MSG gets a new
6976    mid. PING deduplication at the client is on mid. there is no replay
6977    vector at the protocol level.
6978
6979    for offline PING persistence: servers MUST store PINGs keyed by
6980    (uid, mid). duplicate delivery on reconnect is prevented by keying;
6981    a PING for a given (uid, mid) is delivered at most once.
6982
6983  26.4  PING flood via rapid MSG
6984
6985    a sender may send many messages per second each mentioning the same
6986    target, flooding his PING queue.
6987
6988    mitigations (normative):
6989      per-(uid, cid) MSG rate limit applies (%-section_18-%). PING fan-out
6990      is bounded by MSG rate. no additional PING-specific rate limit is
6991      required beyond %-section_18-% MSG limits.
6992
6993      servers SHOULD implement per-(sender-uid, target-uid) PING
6994      coalescing: if sender A has already generated a PING for target B
6995      within a rolling window (RECOMMENDED: 5 seconds), additional PINGs
6996      from A to B in the same window are held and delivered as one event
6997      with count=<n> (see canonical PING event format, %-section_6_8-%).
6998      count is the number of individual PINGs coalesced into this one.
6999      clients SHOULD display count if > 1 (e.g. '5 mentions from user').
7000
7001      server MUST send WARN mid=<latest-mid> code=MENTION_COALESCED
7002      :N pings to <target-uid> coalesced
7003      to the originating client for each target where coalescing occurred.
7004
7005  26.5  adversarial clients: READSTATE manipulation
7006
7007    a client may send READSTATE op=mark for messages it has not received
7008    (guessing mids) or bulk-mark a channel to hide unread counts from
7009    audit trails.
7010
7011    mitigation:
7012      READSTATE is a local user preference; there is no integrity
7013      requirement. a user may mark his own messages seen in any order.
7014      no other user can observe another's READSTATE. there is no security
7015      surface here. server implementations MAY validate that a mid exists
7016      before recording seen state, returning ERR code=NOTFOUND for unknown
7017      mids.
7018
7019  26.6  connection-level resource exhaustion
7020
7021    a client may open many connections, subscribe to many channels, or
7022    send oversized mention lists to exhaust server memory or CPU.
7023
7024    mitigations (normative):
7025      max connections per uid: server-defined (RECOMMENDED: 5).
7026      max SUB count per connection: server-defined (RECOMMENDED: 100).
7027      4096-byte line limit (%-section_1-%) bounds per-line CPU cost.
7028      mention list caps (%-section_26_1-%) bound per-MSG fan-out cost.
7029      servers MAY impose a max members-per-community limit beyond which
7030      'everyone' mentions are rejected with ERR code=TOOBIG.
7031
7032  26.7  cross-shard PING delivery (federation note)
7033
7034    in a federated deployment (%-section_15-%, experimental), a PING for a
7035    uid on a remote server must be forwarded cross-shard. this is an
7036    open problem. current guidance:
7037
7038      - PINGs MUST NOT be forwarded to remote servers in 0.8.
7039      - federated messages that mention users on other servers are
7040        valid but generate PINGs only for local users.
7041      - cross-server PING delivery is reserved for a future s2s
7042        normative revision once the federation trust model is settled.
7043
7044%- server_admin = "Sect.  27" -%
7045========================================================================
7046%-server_admin-% -- SERVER ADMINISTRATION (SRVADM)
7047========================================================================
7048
7049  server administration governs two things that are scoped to the
7050  server itself rather than to any community: who may create communities,
7051  and who holds elevated server-level authority. these are distinct from
7052  community-level roles (%-section_8_2-%) in scope only; the mechanism is
7053  the same (roles, permission tokens, assignment).
7054
7055  27.1  server create policy
7056
7057    servers have a create policy governing who may issue COMM op=create:
7058
7059      open    -- any authenticated user may create a community.
7060                 this is the default if the server does not configure
7061                 a policy explicitly.
7062      granted -- only users explicitly granted the community.create
7063                 permission may create a community. all others receive
7064                 ERR code=NOPERM on COMM op=create.
7065                 servers MAY return ERR code=CHALLENGE instead (%-section_17-%)
7066                 to redirect the user through a verification flow.
7067
7068    clients query the server policy:
7069
7070      SRVADM op=policy
7071      server replies:
7072      SRVADM op=policy create=open|granted
7073      OK verb=SRVADM op=policy
7074
7075    this query is permitted before AUTH (alongside HELLO, CAPS).
7076
7077  27.2  server.admin built-in role
7078
7079    the server has one built-in server-scoped role with the reserved
7080    literal rolid 'server.admin'. a user holding server.admin may:
7081
7082      - grant or revoke community.create for any uid
7083      - assign or revoke server.admin for any uid
7084      - view SRVADM op=list (server member/permission overview)
7085      - override community moderation in any community (implies
7086        community.admin in every community on this server)
7087
7088    there MUST be at least one server.admin at all times after initial
7089    server setup. servers MUST enforce this invariant on revocation:
7090    SRVADM op=role.revoke MUST be rejected with ERR code=FORBIDDEN if
7091    it would leave zero server.admin holders.
7092
7093    unlike the community owner role (%-section_8_0a-%, exactly-one invariant),
7094    server.admin permits multiple simultaneous holders. this asymmetry
7095    is intentional: community ownership is a trust-anchor and legal
7096    accountability concept (exactly one responsible party); server
7097    administration is an operations concept (a team may share it). the
7098    transfer pattern for server.admin is add-then-remove: grant the new
7099    holder with op=role.assign, then revoke the former holder with
7100    op=role.revoke when ready. no atomic swap is required.
7101
7102    the first server.admin is established out-of-band at server
7103    provisioning time (e.g. server config file, first-run setup).
7104    this is the only out-of-band operation in the WIRE lifecycle.
7105    all subsequent admin grants are protocol operations.
7106
7107  27.3  SRVADM operations
7108
7109    all SRVADM ops require server.admin unless noted. exception:
7110    SRVADM op=info uid=<own-uid> (self-query) may be called by any
7111    authenticated uid; see op=info below for the full normative rule.
7112
7113    broadcast behaviour (normative):
7114      SRVADM role grants and permission changes produce no protocol
7115      broadcast. the grantee learns of their new server.admin status
7116      on their next SRVADM op=list query or via an out-of-band channel.
7117      this is intentional: server administration changes are infrequent,
7118      high-trust operations. a broadcast would require all server.admin
7119      holders to receive notifications about each other's grants and
7120      revocations -- an operationally undesirable coupling. unlike
7121      community role assignments (which broadcast to all community
7122      members), server.admin grants are scoped to the server operator
7123      tier and are not a community-visible event.
7124
7125    SRVADM op=role.assign uid=<uid> rolid=server.admin
7126
7127      grants server.admin to the target uid. the acting uid MUST
7128      already hold server.admin.
7129
7130      server replies:
7131      OK verb=SRVADM op=role.assign uid=<uid> rolid=server.admin
7132
7133    SRVADM op=role.revoke uid=<uid> rolid=server.admin
7134
7135      revokes server.admin from the target uid. servers MUST reject
7136      if this would leave zero server.admin holders.
7137
7138      server replies:
7139      OK verb=SRVADM op=role.revoke uid=<uid> rolid=server.admin
7140
7141    SRVADM op=perm.grant uid=<uid> perm=community.create
7142
7143      grants the community.create permission to the target uid.
7144      effective only when server create policy is 'granted'.
7145      on policy 'open', all users have community.create implicitly
7146      and this grant is a no-op (server MAY store it for policy
7147      transitions).
7148
7149      the perm. prefix distinguishes per-uid permission operations from
7150      role operations (op=role.assign / op=role.revoke). server.admin is
7151      a role (assigned/revoked); community.create is a per-uid flag
7152      (granted/revoked). see %-section_24-% for rationale.
7153
7154      server replies:
7155      OK verb=SRVADM op=perm.grant uid=<uid> perm=community.create
7156
7157    SRVADM op=perm.revoke uid=<uid> perm=community.create
7158
7159      revokes the community.create permission from the target uid.
7160
7161      server replies:
7162      OK verb=SRVADM op=perm.revoke uid=<uid> perm=community.create
7163
7164    SRVADM op=list [offset=<n>] [limit=<n>]
7165
7166      enumerates uids with any server-level grant. requires server.admin.
7167      offset default: 0. limit default: 50. limit max: 200.
7168      servers MUST clamp limit to 200.
7169      server replies with one line per matching uid, then OK:
7170      SRVADM op=info uid=<uid> name=<urlencoded-name>
7171             roles=<csv-rolid> perms=<csv-perm> ts=<unix-ms>
7172      ...
7173      OK verb=SRVADM op=list count=<n> total=<n>
7174
7175      roles= includes 'server.admin' if applicable.
7176      perms= lists explicit per-uid grants (e.g. 'community.create').
7177      ts= is the unix-ms of the most recent grant or revoke for this uid.
7178
7179    SRVADM op=info uid=<uid>
7180
7181      queries server-level grant data for a single uid. requires
7182      server.admin, with one exception (see below).
7183
7184      server replies:
7185      SRVADM op=info uid=<uid> name=<urlencoded-name>
7186             roles=<csv-rolid> perms=<csv-perm> ts=<unix-ms>
7187      OK verb=SRVADM op=info uid=<uid>
7188      or
7189      ERR code=NOTFOUND :uid has no server-level grants
7190
7191      NOTFOUND is returned for uids with no server-level role or explicit
7192      grant. this is consistent with COMM op=member.info NOTFOUND for
7193      uids not in a community.
7194      SRVADM op=info is listed in %-section_2_5-% as a routable response verb; it
7195      appears in both SRVADM op=list responses (per-line) and as the
7196      single-line reply to this query. parsers route via %-section_2_5-% pending-
7197      request state; no tag discriminant is needed.
7198
7199      self-query exception (normative):
7200        any authenticated uid MAY call SRVADM op=info uid=<own-uid>
7201        without holding server.admin. this is an explicit carve-out from
7202        the "all SRVADM ops require server.admin" rule, for the following
7203        reason: a uid granted community.create (but not server.admin) has
7204        server-level state that affects what they can do (COMM op=create),
7205        yet has no query path to confirm whether that grant is present or
7206        has been revoked. this violates the "no hidden state" invariant
7207        (%-section_2_6-%). the self-query exception closes this gap at minimal cost:
7208        a uid learns only their own grants, not any other uid's. the
7209        server MUST treat uid=<self> as equivalent to uid=<authenticating-
7210        uid> when applying this exception. requests where uid= does not
7211        match the authenticated uid still require server.admin.
7212        this precedent mirrors WHO: a uid can query their own invisible
7213        status (WHO uid=<self> returns real status) without the general
7214        presence-inspection permission that would allow querying others.
7215
7216  27.4  out-of-band challenges for community.create
7217
7218    servers MAY require a user to complete an out-of-band challenge
7219    before receiving community.create. the entire challenge flow
7220    (CAPTCHA, email verification, account age check, payment, etc.)
7221    happens outside the WIRE protocol, on a server-provided web page
7222    or other external resource.
7223
7224    the protocol surface is minimal:
7225
7226      when COMM op=create is attempted by a uid without community.create
7227      on a create=granted server, the server returns:
7228
7229        ERR code=CHALLENGE url=<https-url> :human-readable description
7230
7231      url= is a percent-encoded HTTPS URL. the client SHOULD present
7232      it to the user (open in browser, display as a link, etc.). the
7233      human-readable payload describes what is required.
7234
7235      after the user completes the challenge out-of-band, the server
7236      grants community.create to the uid. the client retries
7237      COMM op=create and receives OK.
7238
7239    clients MUST NOT attempt to automate or scrape the challenge URL.
7240    the URL is a hint for the human operator, not a machine endpoint.
7241
7242    this approach gives server operators full flexibility (integrate
7243    with any existing identity or payment system) while keeping the
7244    protocol clean. an L0 minimal client displays the URL and instructs the
7245    user to open it. an L0 client that ignores ERR code=CHALLENGE is
7246    simply unable to create communities on gated servers; this is
7247    correct behaviour.
7248
7249%- invite_permissions = "Sect.  28" -%
7250========================================================================
7251%-invite_permissions-% -- INVITE PERMISSIONS
7252========================================================================
7253
7254  invites (COMM op=invite.create, %-section_8_5-%) are the primary mechanism
7255  for controlled community growth. by default only users with
7256  community.admin may create invite codes. the invite.create and
7257  invite.manage permissions allow finer delegation without granting
7258  full admin.
7259
7260  28.1  invite.create
7261
7262    a role with invite.create in its allow set grants its holders the
7263    ability to issue COMM op=invite.create for the community. this is
7264    the Discord "Create Invite" permission analogue.
7265
7266    servers MUST reject COMM op=invite.create from a uid who holds
7267    neither community.admin nor invite.create with ERR code=NOPERM.
7268
7269    invite.create does NOT imply invite.manage. a member who can create
7270    codes cannot see or revoke codes issued by others.
7271
7272    note: redeeming a code uses COMM op=join gid=<gid> code=<code>
7273    (%-section_8_0-%). when the gid is not yet known, the client first calls
7274    COMM op=invite.info code=<code> (%-section_8_5-%) to resolve the gid,
7275    then issues COMM op=join. neither operation requires invite.create;
7276    any authenticated user may call invite.info or redeem a valid code.
7277
7278  28.2  invite.manage
7279
7280    a role with invite.manage grants:
7281      - COMM op=invite.list  : list all active invite codes for a gid
7282      - COMM op=invite.revoke: revoke any invite code for a gid,
7283        including codes issued by other members
7284
7285    a member without invite.manage may still revoke his own codes.
7286
7287  28.3  COMM op=invite.list and the INVITE verb
7288
7289    INVITE is the read verb for invite codes, following the same pattern
7290    as CHAN (channels), ROLE (roles), MEMBER (membership), and EMOJI (emoji).
7291    COMM ops are the write surface; INVITE op=info is the server's response
7292    to both single-code lookup (COMM op=invite.info, %-section_8_5-%) and list
7293    queries (COMM op=invite.list). parsers dispatch on verb then op=.
7294
7295    INVITE op=info serves two contexts with caller-permission-dependent
7296    tag sets. routing is always via %-section_2_5-% pending-request state:
7297
7298      - COMM op=invite.info reply (%-section_8_5-%): carries gid=, code=,
7299        name=, open=, members=, and optionally ttl=, uses=, used=.
7300        creator= and ts= are absent. available to any authenticated user.
7301      - COMM op=invite.list reply (this section): carries gid=, code=,
7302        creator=, uses=, used=, ts=, and optionally ttl=.
7303        name= and members= are absent.
7304        requires invite.manage or community.admin.
7305
7306    the two forms differ only in which optional tags are present,
7307    determined by the caller's permission level. parsers MUST route
7308    INVITE op=info lines via %-section_2_5-% pending-request state (the outstanding
7309    COMM op=invite.info or COMM op=invite.list request); no tag
7310    discriminant is required or defined. a parser MUST NOT use the
7311    presence or absence of any single tag to infer context; only the
7312    pending-request state is authoritative.
7313
7314    the invite.list form omits name= and members= intentionally: invite
7315    management UIs operate within a community that the caller already
7316    belongs to and therefore already has in his local community cache
7317    (fetched via COMM op=info or COMM op=list). duplicating community
7318    metadata on every invite line is wasteful. clients that need
7319    name= or open= for display purposes MUST use their cached COMM op=info
7320    data for the gid; they MUST NOT issue a COMM op=info per code.
7321
7322    recommended sequencing (non-normative):
7323      clients calling COMM op=invite.list SHOULD have previously fetched
7324      COMM op=info gid=<gid> (or received the gid via COMM op=list) to
7325      populate their community name and open= cache for that gid before
7326      presenting invite management UI. the invite.list reply deliberately
7327      omits name= to avoid duplicating data the caller already has; a
7328      client that has not yet fetched COMM op=info will be missing context
7329      it needs for display. this is a UI convenience guideline, not a
7330      protocol requirement: the server has no mechanism to enforce prior
7331      ordering, and no other operation in the protocol imposes a
7332      protocol-level call-ordering prerequisite.
7333
7334    no unsolicited INVITE broadcasts exist in 0.8; invite codes are
7335    administrative data, not live-event data.
7336
7337    COMM op=invite.list gid=<gid> [offset=<n>] [limit=<n>]
7338
7339    offset default: 0. limit default: 50. limit max: 200.
7340    servers MUST clamp limit to 200.
7341
7342    requires invite.manage or community.admin.
7343    server replies with one INVITE line per active (non-expired,
7344    non-exhausted) code in this page, then OK:
7345
7346    INVITE op=info gid=<gid> code=<code> creator=<uid> [uses=<n>] used=<n>
7347           [ttl=<seconds>] ts=<unix-ms>
7348    ...
7349    OK verb=COMM op=invite.list gid=<gid> count=<n> total=<n>
7350
7351    uses= is absent if the code has unlimited redemptions; present with a
7352    positive integer if it has a finite limit. this is identical to the
7353    invite.info reply convention (%-section_8_5-%). ttl= is seconds remaining;
7354    absent if no expiry. used= is how many times the code has been redeemed.
7355    ts= is the unix-ms at which the code was created.
7356    count: number of lines in this page. total: total active codes.
7357
7358  28.4  COMM op=invite.revoke
7359
7360    COMM op=invite.revoke gid=<gid> code=<code>
7361
7362    revokes an invite code immediately. subsequent COMM op=join with
7363    this code returns ERR code=GONE.
7364
7365    permission rules:
7366      - community.admin or invite.manage: may revoke any code.
7367      - invite.create (without invite.manage): may revoke only codes
7368        he himself created (creator uid matches).
7369      - anyone else: ERR code=NOPERM.
7370
7371    server replies:
7372    OK verb=COMM op=invite.revoke gid=<gid> code=<code>
7373
7374  28.5  typical flow (Discord-like)
7375
7376    // server admin creates a Moderator role with invite.create
7377    COMM op=role.create gid=<gid> name=Moderator
7378         perms=invite.create,member.kick
7379    OK verb=COMM op=role.create gid=<gid> rolid=<rolid>
7380
7381    // assigns it to a trusted member
7382    COMM op=role.assign gid=<gid> uid=<mod-uid> rolid=<rolid>
7383    OK verb=COMM op=role.assign gid=<gid> uid=<mod-uid> rolid=<rolid>
7384
7385    // that member can now issue invite codes
7386    COMM op=invite.create gid=<gid> uses=10 ttl=86400
7387    OK verb=COMM op=invite.create gid=<gid> code=AbCd1234
7388
7389    // a new user receives the code (e.g. via a link); resolves gid and
7390    // previews community before joining
7391    COMM op=invite.info code=AbCd1234
7392    INVITE op=info gid=<gid> code=AbCd1234 name=My%20Community
7393           open=0 members=42 ttl=86312 uses=10 used=1
7394
7395    // new user joins
7396    COMM op=join gid=<gid> code=AbCd1234
7397    OK verb=COMM op=join gid=<gid>
7398
7399    // admin inspects all codes; mod can revoke only his own
7400    COMM op=invite.list gid=<gid>   // requires invite.manage
7401    COMM op=invite.revoke gid=<gid> code=AbCd1234
7402    OK verb=COMM op=invite.revoke gid=<gid> code=AbCd1234
7403
7404%- history_purge_e2ee = "Sect.  29" -%
7405========================================================================
7406%-history_purge_e2ee-% -- HISTORY PURGE AND END-TO-END ENCRYPTION
7407========================================================================
7408
7409  29.1  HIST op=purge --- permanent server-side history deletion
7410
7411    cap: purge. servers not advertising purge MUST return ERR code=UNKNOWN
7412    for HIST op=purge.
7413
7414    HIST op=purge deletes all stored events for a channel permanently.
7415    the channel object itself is never deleted; cid, subscriptions,
7416    and future messages are unaffected. only the event history is erased.
7417
7418  send form (client):
7419
7420    HIST op=purge cid=<cid> [before=<unix-ms>]
7421
7422    before= is optional. when present, only events with ts <= before
7423    are deleted. when absent, all events in the channel are deleted.
7424    before= allows time-bounded retention (e.g. "purge events older
7425    than 30 days") without wiping the entire channel.
7426
7427  permission model:
7428
7429    community channels:
7430      the requesting uid MUST hold msg.delete permission in the channel,
7431      OR board.manage on type=board channels (%-section_8_2-%: board.manage
7432      replaces msg.delete for actions on others' content on board channels;
7433      this replacement applies to purge as well).
7434      servers MUST reject with ERR code=NOPERM otherwise.
7435
7436    DM channels:
7437      policy=unilateral: either participant may purge immediately.
7438        server executes on first HIST op=purge from either uid.
7439      policy=mutual: purge requires confirmation from BOTH participants.
7440        first participant's HIST op=purge places the channel in
7441        purge_pending state. the server emits:
7442
7443          HIST op=purge_pending cid=<cid> by=<uid> ts=<unix-ms>
7444             [before=<unix-ms>]
7445
7446        delivered to both participants. the second participant confirms:
7447
7448          HIST op=purge cid=<cid> [before=<unix-ms>]
7449
7450        the server validates that:
7451          1. the cid is in purge_pending state.
7452          2. the confirming uid is the OTHER participant (not the same
7453             uid who issued the initial request).
7454          3. if before= is present in both requests, both values must
7455             match within 1000ms tolerance; if they differ beyond that
7456             the server MUST reject with ERR code=BADARG
7457             :before= mismatch; re-initiate purge to agree on a cutoff.
7458
7459        if the second participant does not confirm within a server-defined
7460        window (RECOMMENDED: 72 hours), the pending state is discarded
7461        silently. either participant may re-initiate.
7462
7463        mutual-policy purge_pending cancellation: either DM participant
7464        may cancel a pending purge:
7465
7466          HIST op=purge_cancel cid=<cid>
7467
7468          server replies:
7469          OK verb=HIST op=purge_cancel cid=<cid>
7470
7471          server broadcasts:
7472          HIST op=purge_cancelled cid=<cid> by=<uid> ts=<unix-ms>
7473
7474        by= in the broadcast identifies which uid cancelled (initiator
7475        or the other participant), allowing clients to display appropriate
7476        UI context ("you cancelled the purge" vs "the other participant
7477        declined the purge").
7478
7479        HIST op=purge_cancel on a cid not in purge_pending state MUST
7480        return ERR code=NOTFOUND :no pending purge for this channel.
7481
7482  server execution (once permission is satisfied):
7483
7484    1. delete all MSG, EDIT, DEL, REACT, UNREACT, PIN, UNPIN, THREAD
7485       event rows for the cid (bounded by before= if present).
7486    2. for each aid that was referenced only by messages in the deleted
7487       set, the server MAY expire the aid immediately. aids that are
7488       still referenced by messages outside the deleted window are
7489       unaffected.
7490    3. READSTATE seen-state entries for (uid, cid, mid) pairs in the
7491       purged set MUST be deleted. the read cursor (READSTATE cursor=)
7492       MUST be reset to absent for the cid if its referenced mid falls
7493       in the purged set.
7494    4. PING entries for mids in the purged set MUST be deleted. servers
7495       MUST NOT deliver PINGs referencing purged mids on reconnect.
7496    5. thread metadata (ThreadStore records) for threads whose opener
7497       mid is in the purged set MUST be deleted.
7498
7499  OK and broadcast:
7500
7501    server replies to the originating client FIRST (commit-before-OK,
7502    %-section_2_6-%):
7503
7504      OK verb=HIST op=purge cid=<cid> [before=<unix-ms>] ts=<unix-ms>
7505         count=<n>
7506
7507    count= is the number of event rows deleted. ts= is the unix-ms of
7508    the purge operation.
7509
7510    server then broadcasts to ALL current subscribers of cid:
7511
7512      HIST op=purged cid=<cid> [before=<unix-ms>] by=<uid> ts=<unix-ms>
7513
7514    clients receiving HIST op=purged MUST purge their local message cache
7515    for the cid (bounded by before= if present). subscription state,
7516    read cursor, and PING state for the cid are ALSO cleared by the
7517    client for any entries in the purged window.
7518
7519  HIST op=purge verb directionality (normative):
7520    parsers MUST use op= value to distinguish variants:
7521      op=purge         -- client-sent request.
7522      op=purge_pending -- server-broadcast (mutual policy, first step).
7523      op=purge_cancel  -- client-sent cancellation request.
7524      op=purge_cancelled -- server-broadcast cancellation confirmation.
7525      op=purged        -- server-broadcast (purge complete).
7526    by= is present only in server-sent lines and MUST NOT be sent
7527    by clients. ts= is present only in server-sent lines.
7528
7529  interaction with HIST queries in flight:
7530
7531    if a HIST query is in progress on cid at the time HIST op=purge
7532    is processed, the server MUST complete the in-flight HIST response
7533    before executing the purge. this follows the same rule as COMM
7534    op=delete (%-section_8_0-%). a HIST query that begins after the purge
7535    commit MUST return an empty result for the purged window.
7536
7537  interaction with before= cursor in HIST queries:
7538
7539    after a purge with before=<T>, HIST queries using before= or after=
7540    cursors that fall within the purged window return zero events for
7541    that window. has_more=0 is returned when no events exist beyond the
7542    window in the requested direction.
7543
7544  adversarial considerations:
7545
7546    a malicious uid with msg.delete permission on a community channel
7547    could issue HIST op=purge to destroy evidence of their own misconduct.
7548    server operators SHOULD implement out-of-band audit logs that are
7549    not subject to the purge operation, or restrict msg.delete (and
7550    therefore purge) to community.admin holders only via role configuration.
7551    the server's own append-only audit trail (outside the WIRE protocol)
7552    is the appropriate long-term evidence store; WIRE does not guarantee
7553    tamper-evidence of its event log.
7554
7555  ==========================================================================
7556
7557  29.2  end-to-end encryption (E2E)
7558
7559    cap: e2e. servers not advertising e2e MUST relay MSG enc= lines
7560    opaquely (verbatim, without ERR). servers advertising e2e support the
7561    full surface defined in this section: PUBKEY op=prekey, op=prekey.fetch,
7562    op=keysync; COMM op=channel.e2e; CHAN op=e2e; MSG enc=skdm; and
7563    offline keysync pending delivery (%-section_29_2c-%).
7564
7565    design goals:
7566      - one ciphertext on the wire per message, regardless of group size.
7567        the server never needs to know member count to relay a message.
7568      - servers relay ciphertext without ever seeing plaintext.
7569      - key distribution is on-protocol; no out-of-band PKI.
7570      - DMs and community channels use the same primitives and the same
7571        wire surface. the server cannot distinguish them; both are opaque.
7572      - enc= is opaque metadata at the routing and storage layer.
7573        routing verbs (DEL, REACT, PIN, THREAD) apply to encrypted
7574        messages without modification; HIST excludes enc=skdm lines
7575        (%-section_29_2b-%); EDIT requires the replacement line to carry
7576        enc=xchacha20 and cannot change enc= type (%-section_29_2e-%).
7577      - forward secrecy: compromise of current key material does not
7578        expose past messages.
7579      - break-in recovery: compromise is healed automatically as ratchet
7580        state advances.
7581      - membership change security: when a member is removed, they cannot
7582        decrypt future messages after senders distribute new keys to the
7583        updated member set.
7584
7585  dumb-relay invariant (normative): the server stores and relays
7586  ciphertext verbatim. it MUST NOT decrypt, re-encrypt, or transform
7587  stored ciphertext for any reason, including policy changes, history
7588  operations, or administrative actions.
7589
7590  two-layer model:
7591
7592    Layer 1 --- pairwise sessions (X3DH + Double Ratchet):
7593      used to establish a secure 1:1 channel between any two uids. used
7594      directly for DM encryption AND as the secure transport for
7595      distributing group Sender Keys to individual members.
7596
7597    Layer 2 --- Sender Keys (for channels with a gid):
7598      each sender maintains one Sender Key per (uid, cid). the sender
7599      encrypts the message payload once; recipients decrypt independently
7600      using their locally-cached copy of that sender's key for that
7601      channel. the Sender Key is distributed to each member via a
7602      pairwise Layer 1 session. this is Signal's SenderKey protocol.
7603
7604    DMs (no gid) use Layer 1 directly: X3DH session setup + Double
7605    Ratchet for all messages. no Sender Keys are used.
7606
7607    community channels (gid present, e2e=1) use Layer 2. one ciphertext
7608    per message on the wire regardless of member count.
7609
7610    the wire surface is identical for both: MSG enc=xchacha20. the server
7611    cannot and need not distinguish them.
7612
7613  ==========================================================================
7614
7615  29.2a  identity and pairwise session keys (Layer 1)
7616
7617  key types:
7618
7619    Ed25519 identity keys are the source of truth for uid derivation
7620    (%-section_3-%). for key agreement, WIRE uses X25519 (Diffie-Hellman over
7621    Curve25519). Ed25519 and X25519 use the same underlying curve but
7622    different point representations; they MUST NOT be mixed.
7623
7624    each uid that participates in E2E maintains:
7625
7626      identity key pair:   long-term Ed25519 key (required by %-section_3-% and %-section_4-%).
7627      X25519 key pair:     independently generated for ECDH. servers
7628                           MUST NOT derive X25519 keys from Ed25519 keys.
7629                           clients MUST generate X25519 keys independently
7630                           using a CSPRNG.
7631      one-time prekeys:    a set of ephemeral X25519 key pairs (OPKs).
7632                           each OPK is used for exactly one session setup
7633                           and then discarded. clients SHOULD maintain
7634                           at least 20 OPKs on the server at all times.
7635
7636  prekey bundle format (stored on server, distributed on fetch):
7637
7638      ik=<b64url-X25519-pubkey>    -- long-term X25519 identity key
7639      spk=<b64url-X25519-pubkey>   -- signed prekey (rotated periodically)
7640      spk_sig=<b64url-Ed25519-sig> -- Ed25519 sig over spk, signed by the
7641                                      uid's Ed25519 identity key. binds the
7642                                      X25519 spk to the uid's identity.
7643      opk=<b64url-X25519-pubkey>   -- one-time prekey (server picks one;
7644                                      absent if OPK pool exhausted).
7645
7646    clients MUST verify spk_sig before using any prekey bundle.
7647
7648  PUBKEY op=prekey  (client -> server, publish own bundle):
7649
7650    PUBKEY op=prekey ik=<b64url> spk=<b64url> spk_sig=<b64url>
7651           [opk_count=<n>] :<newline-separated-b64url-OPKs>
7652
7653    publishes the sending uid's prekey bundle. any authenticated uid may
7654    call this for its own uid; MUST NOT be called for another uid.
7655    ik= and spk= are X25519 public keys (32 bytes, base64url, no padding).
7656    spk_sig= is the Ed25519 signature of spk= bytes, signed with the
7657    uid's Ed25519 private key.
7658    payload contains zero or more OPK public keys (X25519, b64url), one
7659    per \n-separated line (%-section_2_4-%).
7660    a repeated call replaces ik= and spk= and APPENDS to the OPK pool.
7661
7662    server replies:
7663    OK verb=PUBKEY op=prekey uid=<uid> opk_count=<n>
7664
7665    opk_count= is the total OPKs now stored for this uid after the update.
7666    clients use this to decide whether to upload additional OPKs.
7667
7668    servers MUST reject spk_sig= that does not verify against the uid's
7669    registered Ed25519 pubkey:
7670    ERR code=AUTHFAIL :spk_sig verification failed
7671
7672    servers MAY enforce an OPK pool maximum (RECOMMENDED: 200). uploads
7673    beyond the maximum are silently discarded; opk_count= reflects the
7674    actual pool size.
7675
7676    directionality (normative): client-sent only, identified by presence
7677    of ik=. servers MUST NOT send PUBKEY op=prekey unsolicited.
7678
7679  PUBKEY op=prekey.fetch  (client -> server, fetch another uid's bundle):
7680
7681    PUBKEY op=prekey.fetch uid=<uid>
7682
7683    fetches one prekey bundle for the target uid. the server atomically
7684    consumes one OPK (removes it from the pool so it is not reissued).
7685    any authenticated uid may fetch any other uid's bundle.
7686
7687    server replies:
7688    PUBKEY op=prekey uid=<target-uid> ik=<b64url> spk=<b64url>
7689           spk_sig=<b64url> [opk=<b64url>]
7690    OK verb=PUBKEY op=prekey.fetch uid=<target-uid>
7691
7692    opk= is absent if the pool is exhausted. the fetching client SHOULD
7693    still proceed using only ik= and spk= (no-OPK X3DH variant; weaker
7694    forward secrecy for that session only, but correct and interoperable).
7695    the target uid is notified:
7696      WARN code=OPK_DEPLETED :prekey pool exhausted; upload new OPKs
7697    (defined in the WARN code table at %-section_26_0-%.)
7698
7699    directionality (normative): client sends op=prekey.fetch with uid= as
7700    the target. server reply carries op=prekey with bundle tags. parsers
7701    use op= to distinguish: op=prekey.fetch is always client-sent;
7702    op=prekey with ik= in server context is the bundle reply. routing
7703    via %-section_2_5-% pending-request state.
7704
7705  session setup (X3DH, non-normative summary):
7706
7707    initiator fetches target's bundle via PUBKEY op=prekey.fetch.
7708    initiator performs X3DH using the bundle to derive a shared secret.
7709    the first message carries ephemeral public key material inline in
7710    the ciphertext header (Signal X3DH specification) so the target can
7711    derive the same shared secret on receipt without a prior round-trip.
7712    subsequent messages use the Double Ratchet algorithm for forward
7713    secrecy and break-in recovery. key derivation and ratchet state are
7714    entirely client-side; the server stores only ciphertext.
7715
7716  ==========================================================================
7717
7718  29.2b  Sender Keys (Layer 2 --- community channels)
7719
7720    Sender Keys are used for channels where e2e=1 and the channel has a
7721    gid. DMs (no gid) always use Layer 1 directly.
7722
7723  sender key concept:
7724
7725    each sender (uid) maintains one Sender Key per (uid, cid):
7726
7727      SKS_id    -- Sender Key ID: 4 random bytes (base64url). used by
7728                   recipients as a lookup hint for the correct key.
7729      SKS_chain -- current chain key (32 bytes). advanced per message
7730                   via HKDF to derive per-message keys (one-way ratchet).
7731                   compromising the chain at message N does not expose
7732                   messages 0..N-1.
7733      SKS_sig   -- Ed25519 signing key pair. the sender signs Sender Key
7734                   distribution messages and per-message plaintext to
7735                   authenticate the source and prevent key substitution.
7736
7737  sender key distribution message (SKDM):
7738
7739    before sending encrypted messages on a channel, the sender must
7740    distribute their Sender Key to every current member who does not
7741    already hold it. distribution uses a pairwise Layer 1 session per
7742    recipient.
7743
7744    an SKDM is a MSG with enc=skdm:
7745
7746      MSG cid=<cid> enc=skdm target=<uid> :<ciphertext-b64url>
7747
7748    the payload is a pairwise-encrypted (Layer 1) blob carrying
7749    (SKS_id, SKS_chain_seed, SKS_sig_pubkey), signed with the sender's
7750    Ed25519 identity key. the target decrypts using their Layer 1 session
7751    with the sender.
7752
7753    enc=skdm rules (normative):
7754
7755      target= is REQUIRED. servers MUST reject MSG enc=skdm without
7756      target= with ERR code=BADARG :enc=skdm requires target=.
7757      servers MUST reject MSG enc=skdm where target= is not a current
7758      community member with ERR code=BADARG :target= is not a member.
7759
7760      enc=skdm lines are relayed to ALL subscribers of cid. only the
7761      target uid can decrypt the payload (pairwise-encrypted). non-target
7762      subscribers MUST ignore enc=skdm lines where target= does not
7763      match their own uid.
7764
7765      enc=skdm lines are NOT stored in HIST. servers MUST NOT return
7766      enc=skdm lines in HIST responses. SKDMs are ephemeral key-
7767      distribution events, not conversation history.
7768
7769      enc=skdm lines do NOT generate PING events. servers MUST suppress
7770      PING generation for enc=skdm lines.
7771
7772      enc=skdm lines do NOT participate in READSTATE, REACT, PIN,
7773      THREAD, or EDIT. servers MUST reject REACT, PIN, THREAD, or EDIT
7774      referencing a mid assigned to an enc=skdm message:
7775      ERR code=BADARG :cannot reference a key-distribution message.
7776
7777      enc=skdm lines receive a server-assigned mid= (needed for ordering
7778      and to enforce the above rejections).
7779
7780    SKDM payload size: the pairwise-encrypted payload is approximately
7781    200 bytes raw (~267 bytes base64url), well within the 4096-byte limit.
7782
7783  sending an encrypted message (Layer 2):
7784
7785    once the Sender Key has been distributed per %-section_29_2c-%:
7786
7787      MSG cid=<cid> enc=xchacha20 :<ciphertext-b64url>
7788
7789    recommended ciphertext structure (non-normative):
7790      [4 bytes]  SKS_id (recipient lookup hint; not a security parameter)
7791      [4 bytes]  chain iteration counter (uint32 big-endian)
7792      [24 bytes] XChaCha20-Poly1305 nonce
7793      [N+16 bytes] XChaCha20-Poly1305 ciphertext + 16-byte auth tag
7794
7795    the enc=xchacha20 tag is identical for DM (Layer 1) and channel
7796    (Layer 2) messages. the server dumb-relays both identically.
7797
7798  ==========================================================================
7799
7800  29.2c  key distribution lifecycle
7801
7802  initial distribution (sender first sending encrypted on a channel):
7803
7804    the client MUST:
7805      1. generate a fresh Sender Key for (uid, cid).
7806      2. for every current member of the channel who does not already
7807         hold the sender's current Sender Key: fetch their prekey bundle
7808         via PUBKEY op=prekey.fetch (these requests MAY be pipelined per
7809         %-section_2_5-%: distinct uid= targets may be pipelined freely).
7810      3. send MSG enc=skdm target=<uid> for each such member.
7811      4. begin sending MSG enc=xchacha20.
7812
7813    steps 2--3 are pipelined: the client MAY send all SKDMs before the
7814    first encrypted MSG. the server relays them in order.
7815
7816  receiving a Sender Key (recipient):
7817
7818    on receiving MSG enc=skdm target=<own-uid>:
7819      1. decrypt the payload using the pairwise Layer 1 session with the
7820         sender (uid= of the MSG line).
7821      2. verify the Ed25519 signature over the SKDM content.
7822      3. store (SKS_id, SKS_chain_seed, SKS_sig_pubkey) keyed by
7823         (cid, sender-uid, SKS_id) in the client's local key store.
7824
7825    on receiving MSG enc=xchacha20 on an e2e=1 channel:
7826      1. parse the SKS_id hint from the payload header.
7827      2. look up the cached Sender Key for (cid, sender-uid, SKS_id).
7828      3. advance the chain to the counter position; derive per-message key.
7829      4. decrypt and verify the Ed25519 signature.
7830      5. if no matching Sender Key found: display "waiting for key" and
7831         send PUBKEY op=keysync (see catch-up below).
7832
7833    out-of-order messages: clients SHOULD cache skipped per-message keys
7834    up to a window of 2000 skipped keys. beyond this window, messages are
7835    unrecoverable. this matches Signal's SenderKey specification.
7836
7837  catch-up (member who missed SKDMs):
7838
7839    a member who was offline, freshly joined, or whose client was wiped
7840    may receive MSG enc=xchacha20 lines they cannot decrypt. catch-up:
7841
7842      PUBKEY op=keysync cid=<cid> uid=<target-sender-uid>
7843        (client -> server: "I need this sender's Sender Key for this channel")
7844
7845    server handling:
7846      1. replies immediately to the requesting client:
7847         OK verb=PUBKEY op=keysync cid=<cid> uid=<target-sender-uid>
7848      2. routes the keysync to the target sender's active connections:
7849         PUBKEY op=keysync cid=<cid> uid=<requesting-uid>
7850         (uid= is rewritten to the requester; sender knows who needs SKDM)
7851      3. if the target sender has no active connections, the server MUST
7852         store the keysync request and deliver it on the sender's next
7853         AUTH OK (before the PING burst), preceded by WARN
7854         code=SKDM_PENDING (%-section_26_0-%). stored requests are keyed by
7855         (requesting-uid, sender-uid, cid). the server clears the stored
7856         request when the sender issues MSG enc=skdm cid=<cid>
7857         target=<requesting-uid>.
7858
7859    the target sender responds by issuing:
7860      MSG cid=<cid> enc=skdm target=<requesting-uid> :...
7861
7862    directionality (normative): PUBKEY op=keysync is bidirectional.
7863    client-sent: cid= and uid= identify the sender whose key is needed.
7864    server-routed delivery: same verb, same tags, but uid= is rewritten
7865    to the requester (uid= asymmetry; see rationale in %-section_29_2f-%).
7866    parsers MUST use session context (pending-request state for the
7867    client-sent OK; unsolicited for the server-routed delivery) to
7868    distinguish direction. servers MUST NOT send PUBKEY op=keysync to
7869    clients that did not send it first, except as routed deliveries for
7870    keysync requests those clients are the target of.
7871
7872  new member joining an e2e=1 channel:
7873
7874    on receiving MEMBER op=join gid=<gid> uid=<new-uid>, each active
7875    sender SHOULD proactively issue an SKDM to the new member for their
7876    current Sender Key on that channel. this is a SHOULD: senders may
7877    be offline. offline senders are reached via PUBKEY op=keysync when
7878    the new member first encounters a message they cannot decrypt.
7879
7880  member removal --- forward secrecy:
7881
7882    on receiving MEMBER op=kick, MEMBER op=ban, or MEMBER op=leave for
7883    any uid in a gid, each remaining sender MUST rotate their Sender Key
7884    for all channels in that gid before sending the next encrypted message:
7885      1. generate a new Sender Key (new SKS_id, new SKS_chain).
7886      2. distribute the new Sender Key to all REMAINING members (not the
7887         removed uid) via new SKDMs.
7888      3. use the new Sender Key for all subsequent messages.
7889
7890    clients MAY batch: send all channel SKDMs for the gid before resuming
7891    messaging. senders that have no remaining messages to send need not
7892    rotate immediately, but MUST rotate before their next encrypted message.
7893
7894    the removed uid retains cached copies of old Sender Keys and can
7895    decrypt messages they received before rotation. messages sent after
7896    rotation are unreadable to them. this is the intended security property.
7897
7898    periodic rotation (RECOMMENDED): clients SHOULD also rotate their
7899    Sender Key after 1 week or 1000 messages on a channel, whichever
7900    comes first. this bounds break-in recovery time independently of
7901    membership changes.
7902
7903  ==========================================================================
7904
7905  29.2d  channel E2E policy
7906
7907    E2E policy is a property of the CHANNEL RECORD. a community may have
7908    some channels with e2e=1 and others with e2e=0. policy is per-channel,
7909    not per-community.
7910
7911  setting E2E policy at channel creation:
7912
7913    COMM op=channel.create accepts e2e=0|1 (default: 0; full verb
7914    definition in %-section_8_4-%).
7915
7916    e2e=1: all MSG on this channel MUST carry enc=xchacha20 or enc=skdm.
7917    e2e=0 (default): unencrypted MSG is permitted; senders may still use
7918    enc=xchacha20 voluntarily and the server relays it opaquely.
7919
7920    servers MUST reject e2e=1 on type=voice channels:
7921    ERR code=BADARG :voice channels do not carry message content; e2e= is not applicable
7922    (voice audio travels on a separate VFRAME connection. the WIRE channel
7923    for voice carries only signaling; signaling is not content to encrypt.)
7924
7925  changing E2E policy after creation:
7926
7927    COMM op=channel.e2e gid=<gid> cid=<cid> e2e=0|1
7928
7929    requires channel.manage permission (%-section_8_2-%).
7930    servers MUST verify gid+cid co-membership (%-section_8_4-% co-validation rule).
7931
7932    server reply:
7933    OK verb=COMM op=channel.e2e gid=<gid> cid=<cid> e2e=0|1
7934
7935    server broadcast to all community members:
7936    CHAN op=e2e gid=<gid> cid=<cid> e2e=0|1 uid=<acting-uid> ts=<unix-ms>
7937
7938    non-retroactivity (normative):
7939      changing e2e= does not affect stored messages. HIST returns messages
7940      verbatim: enc= lines stay enc=; plaintext lines stay plaintext. the
7941      policy boundary is the ts= of the CHAN op=e2e event. clients
7942      rendering HIST MUST respect enc= on each individual MSG line rather
7943      than inferring encryption state from the channel's current policy.
7944      servers MUST NOT re-encrypt or decrypt stored messages on policy
7945      change (dumb-relay invariant, %-section_29_2-%).
7946
7947  CHAN op=info for channels:
7948
7949    e2e=0|1 is a first-class stored field on the channel record. servers
7950    MUST include it in all CHAN op=info responses. it is never derived
7951    from another record.
7952
7953    CHAN op=info gid=<gid> cid=<cid> slug=<slug> name=<urlencoded-name>
7954         type=text|voice|forum|board e2e=0|1 ts=<unix-ms>
7955         [:topic]
7956
7957  DM channel E2E policy:
7958
7959    DM channels (no gid) set e2e= at creation via the SUB call (%-section_5-%,
7960    DM creation policy fields). e2e= is immutable after creation; the same
7961    immutability, rejection, and SUB OK echo rules that apply to
7962    delete_policy= apply to e2e= identically. rationale: a participant who
7963    opened an encrypted DM before sharing sensitive content made a trust
7964    agreement; immutability makes the policy a verifiable contract for the
7965    lifetime of the channel. users who want different terms must open a new
7966    DM channel.
7967
7968    servers MUST reject e2e= on SUB for a non-DM cid:
7969    ERR code=BADARG :use COMM op=channel.e2e for community channels
7970
7971    DM E2E key model: DMs use Layer 1 (X3DH + Double Ratchet) only.
7972    enc=skdm is NOT used on DM channels. a DM with e2e=1 requires all
7973    MSG to carry enc=xchacha20; plaintext MSG is rejected by the server.
7974
7975    client enforcement (normative): clients reading e2e=1 from CHAN
7976    op=info MUST refuse to send plaintext MSG on that channel. this
7977    applies regardless of whether the server advertises cap=e2e; servers
7978    that do not cannot enforce the policy server-side, making client
7979    enforcement the last line of defense.
7980
7981  ==========================================================================
7982
7983  29.2e  server enforcement
7984
7985    on MSG without enc= on a channel where e2e=1:
7986    ERR code=NOPERM :this channel requires end-to-end encryption
7987
7988    on EDIT for a mid that was originally enc=xchacha20, if EDIT
7989    omits enc= or changes it to a different value:
7990    ERR code=BADARG :enc= type cannot change on edit
7991
7992    on MSG enc=skdm without target=:
7993    ERR code=BADARG :enc=skdm requires target=
7994
7995    on MSG enc=skdm where target= is not a current community member:
7996    ERR code=BADARG :target= is not a member of this community
7997
7998    on REACT, PIN, THREAD, or EDIT referencing a mid from an enc=skdm MSG:
7999    ERR code=BADARG :cannot reference a key-distribution message
8000
8001    enc=skdm relay (normative): servers MUST relay MSG enc=skdm to ALL
8002    current subscribers of cid (not only target= uid). client-side
8003    filtering on target= is normative. the server is a dumb relay and
8004    MUST NOT attempt selective delivery based on target=.
8005
8006    servers NOT advertising cap=e2e MUST relay MSG enc= lines verbatim
8007    without error (opaque relay rule, unchanged). they cannot enforce
8008    e2e= channel policy; client-side enforcement is normative (%-section_29_2d-%).
8009
8010  enc= interactions with other verbs:
8011
8012    enc= and HIST:
8013      HIST returns enc=xchacha20 MSG lines with the ciphertext payload
8014      verbatim (dumb-relay invariant, %-section_29_2-%).
8015      HIST MUST NOT return enc=skdm lines (excluded per %-section_29_2b-%).
8016
8017    enc= and EDIT:
8018      EDIT may replace the ciphertext payload of an encrypted message.
8019      the EDIT line MUST carry enc=xchacha20. clients MUST NOT switch
8020      enc= values. servers enforce per %-section_29_2e-% above.
8021
8022    enc= and DEL:
8023      DEL operates on encrypted messages identically to plaintext.
8024      the server tombstones the mid; encrypted content is permanently
8025      lost since the server never held the plaintext.
8026
8027    enc= and HIST op=purge:
8028      HIST op=purge operates on enc=xchacha20 messages identically to
8029      plaintext messages. ciphertext rows are deleted. unrecoverable.
8030
8031    enc= and REACT / PIN / THREAD:
8032      reactions, pins, and thread replies may reference enc=xchacha20
8033      mids without restriction. the server does not require enc= on
8034      those operations and does not parse the ciphertext.
8035
8036    enc= and line length:
8037      clients MUST ensure total line length does not exceed 4096 bytes
8038      (%-section_1-%). for Layer 2 (Sender Key) messages: 8 bytes header + 24-byte
8039      nonce + 16-byte auth tag = 48 bytes crypto overhead. derivation:
8040      4096 bytes total - 1 LF - ~55 bytes line prefix ("MSG cid=<26-char-
8041      ulid> enc=xchacha20 :") = ~4040 bytes for base64url ciphertext;
8042      base64url decodes at 3 bytes per 4 chars = ~3030 bytes raw; minus
8043      48 bytes overhead = ~2980 bytes theoretical maximum. clients MUST
8044      use a conservative limit of 2710 bytes to accommodate variable-
8045      length cid values and future tag additions. clients MUST split
8046      messages exceeding this limit.
8047
8048  ==========================================================================
8049
8050  29.2f  rationale
8051
8052  why Sender Keys for channels instead of pairwise X3DH per recipient?
8053
8054    pairwise X3DH for channels requires the sender to encrypt the message
8055    body N times and send N MSG lines per message. for a channel with 100
8056    members, one message generates 100 wire lines: O(N) server storage,
8057    O(N) relay bandwidth, O(N) encryption work. this violates the
8058    "one line per logical unit" design goal (design goal 4, preamble).
8059
8060    Sender Keys solve this: the sender encrypts once, sends one MSG line.
8061    all members decrypt independently from their cached key copy. O(1)
8062    server work per message; O(N) only at key distribution time, amortised
8063    over many messages. this is how Signal, WhatsApp, and iMessage group
8064    chats work.
8065
8066  why does enc=skdm go to all subscribers and not just the target?
8067
8068    the server is a dumb relay. routing MSG lines selectively to a subset
8069    of subscribers requires per-message recipient tracking and conditional
8070    delivery suppression --- server-side logic that contradicts the dumb-
8071    relay design and creates hidden state (%-section_2_6-%). target= gives clients
8072    the information to self-filter. the payload is pairwise-encrypted;
8073    non-target subscribers cannot decrypt it even if they receive it.
8074    there is no privacy loss from broadcasting SKDMs to all subscribers.
8075
8076  why is enc=skdm excluded from HIST?
8077
8078    SKDMs are key-distribution events, not conversation content. a member
8079    reconnecting later needs fresh SKDMs via the keysync path, not stale
8080    ones from HIST. stale SKDMs in HIST would deliver historical key
8081    material to a client that could not previously decrypt those messages
8082    (privacy regression). the live distribution path always delivers
8083    current keys. exclusion from HIST is therefore both a correctness
8084    and a privacy requirement.
8085
8086  why is DM e2e= immutable but channel e2e= mutable?
8087
8088    DM immutability: see DM channel E2E policy in %-section_29_2d-%. users who want
8089    different terms open a new DM.
8090
8091    channel mutability: an admin disabling E2E is a community-wide policy
8092    decision, analogous to changing open=. the change is transparent via
8093    CHAN op=e2e broadcast and non-retroactive. members see the event and
8094    may choose to leave. this is acceptable; a unilateral change to a
8095    1:1 agreement is not.
8096
8097  why is Sender Key rotation a MUST on member removal?
8098
8099    a removed member must not be able to decrypt future messages. this is
8100    the fundamental post-removal privacy guarantee. making it a MUST
8101    ensures all compliant clients enforce it. note: the removed member
8102    still holds old Sender Keys and can decrypt messages from before
8103    removal --- this is unavoidable without retroactive re-encryption,
8104    which the server cannot perform (no plaintext). the forward property
8105    (future messages unreadable) is what matters and what MUST rotation
8106    achieves.
8107
8108  why is periodic rotation a SHOULD and not a MUST?
8109
8110    periodic rotation is a break-in-recovery mechanism. if a current chain
8111    key is compromised, rotation limits the window of exposure. making it
8112    MUST would require clients to emit bursts of SKDMs on a schedule even
8113    on idle channels. the protocol provides the mechanism; the schedule is
8114    an application-level quality-of-security commitment. rotation on
8115    member events (MUST) is the hard correctness requirement.
8116
8117  why X3DH + Double Ratchet for DMs, not Sender Keys?
8118
8119    Sender Keys require distributing an SKDM before the first message.
8120    for a DM this adds latency: fetch prekey, generate Sender Key, send
8121    SKDM, then send first message. X3DH solves this naturally: the
8122    initiator includes ephemeral key material inline in the first
8123    ciphertext (Signal X3DH format) so the recipient derives the shared
8124    secret on receipt with no prior round-trip. DMs have exactly two
8125    parties; the O(N) problem that motivates Sender Keys does not arise.
8126    X3DH + Double Ratchet is strictly superior for 2-party conversations.
8127
8128  why PUBKEY op=keysync instead of distributing SKDMs via HIST?
8129
8130    SKDMs are excluded from HIST (see above). a catch-up mechanism is
8131    therefore necessary. keysync is a targeted pull: the recipient asks
8132    a specific sender for their current Sender Key. this is better than
8133    a broadcast request (all senders respond unnecessarily) or polling.
8134    the persistent delivery model (stored if sender is offline) mirrors
8135    PING: the server holds state until the target is available, then
8136    delivers on reconnect. keysync is precise, low-noise, and privacy-
8137    preserving: only the two parties involved learn that a key was needed.
8138
8139  why does PUBKEY op=keysync use uid= to mean different things in each direction?
8140
8141    PUBKEY op=keysync is the only bidirectional verb in the protocol
8142    where uid= identifies different principals in each direction: the
8143    target sender in the client-sent form; the requester in the server-
8144    routed form. all other bidirectional verbs use uid= to identify the
8145    acting user in both directions.
8146
8147    the tradeoff: a from= tag on the server-routed form would eliminate
8148    the asymmetry, but it would require a protocol change, extra server
8149    logic for one verb, and a new tag with no other consumer. the session-
8150    context discriminant (pending-request vs. unsolicited) is sufficient
8151    for all conforming parsers. a future from= extension is reserved; it
8152    MUST be adopted if a second verb requires the same convention
8153    (see %-section_25-%).
8154
8155  why E2E uses X25519 separately from Ed25519?
8156
8157    Ed25519 and X25519 are both Curve25519 operations but in different
8158    groups and with different point representations. converting an Ed25519
8159    private key to X25519 is mathematically defined (cofactor clearing,
8160    clamp), but doing so ties the long-term signing key to the key
8161    agreement key. if the key agreement key is compromised via oracle or
8162    side-channel, the signing key and therefore the uid is compromised too.
8163    using an independently generated X25519 key pair limits blast radius:
8164    a broken session key does not threaten uid integrity. this is the same
8165    rationale used by Signal and TLS 1.3.
8166
8167  why X3DH and Double Ratchet are RECOMMENDED but not normative?
8168
8169    the protocol defines what goes on the wire (prekey bundle format, enc=
8170    tag, ciphertext payload as opaque base64url). the key derivation and
8171    ratchet algorithm are client-to-client contracts; the server never
8172    touches plaintext. mandating a specific KDF would force all future
8173    client implementations to implement Signal's exact math, including
8174    future key types and post-quantum variants. keeping the algorithm non-
8175    normative allows algorithm agility while keeping the transport protocol
8176    stable.
8177
8178    scope of this non-normativity (privacy vs interoperability):
8179      algorithm non-normativity is an INTEROPERABILITY concern, not a
8180      security concern. a client that deviates from X3DH + Double Ratchet
8181      will fail to establish sessions with conforming clients --- the X3DH
8182      handshake produces different shared secrets and ciphertext becomes
8183      undecryptable. messages fail silently; no plaintext is exposed to
8184      the server, to other participants, or to anyone else. deviation
8185      breaks the deviating client's own sessions. it does not weaken
8186      any other participant's privacy.
8187
8188    the one privacy concern the protocol cannot address is a client that
8189    correctly implements the cryptography but exfiltrates plaintext or
8190    leaks private keys after decryption. this is outside the threat model
8191    of any E2E protocol, including Signal: once an attacker controls the
8192    endpoint, the protocol offers no protection. this is equivalent to
8193    a modified client install, not a protocol weakness.
8194
8195  why PUBKEY op=prekey.fetch consumes an OPK atomically?
8196
8197    one-time prekeys are designed to be single-use. if a server issued
8198    the same OPK to two different initiators, both sessions would share
8199    the same ephemeral key contribution and the "O" in OPK would be
8200    void. atomic consumption prevents this. the no-OPK fallback (session
8201    proceeds with only ik= and spk=) is explicitly specified so that OPK
8202    exhaustion degrades gracefully rather than breaking E2E entirely.
8203    the OPK_DEPLETED WARN prompts the target to replenish without
8204    requiring the initiator to poll.
8205
8206  ==========================================================================
8207
8208  WARN codes for E2E:
8209
8210    OPK_DEPLETED and SKDM_PENDING are defined in the canonical WARN code
8211    table at %-section_26_0-%. their normative definitions, mid= tag presence rules,
8212    and client MUST requirements are authoritative there. the definitions
8213    are not repeated here to avoid divergence.
8214
8215
8216========================================================================
8217END OF WIRE SPECIFICATION %-version-%
8218========================================================================
8219
8220  this document is not final. implementation experience will revise it.
8221  send corrections to the wire-wg issue tracker.
8222  ISC license applies to all reference code in this document.