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  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  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  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.