mooncat

A native ASGI 3.0 server for MoonBit. It runs a moonasgi app over a real network socket — accept a connection, turn each request into a Scope / Receive / Send, and drive your app — exactly the role uvicorn plays for Python.

CItestsGitHublicense
$moon add moonbitstack/mooncat

✶ The contract at a glance

pub async fn serve(app : AsgiApp, host? : String, port? : Int, backlog? : Int) -> Unit
pub async fn serve_config(app : AsgiApp, config : Config) -> Unit
pub async fn serve_tls(app : AsgiApp, certificate_file~, private_key_file~, pfx_file~, ...) -> Unit
pub async fn serve_graceful(app : AsgiApp, config : Config, handle? : ShutdownHandle) -> Unit
pub async fn serve_h2c(app : AsgiApp, host? : String, port? : Int) -> Unit

// One socket -> Scope / Receive / Send -> your moonasgi app:
//   HTTP/1.1 request  ->  Http scope     ->  HttpResponseStart + HttpResponseBody
//   HTTP/2 (h2c)      ->  Http scope     ->  HEADERS + DATA, flow-controlled
//   Upgrade: websocket ->  WebSocket scope -> connect/accept/receive/send/close
//   process boundary  ->  Lifespan scope ->  startup ... shutdown
//   handle.shutdown() ->  stop accepting -> drain -> lifespan shutdown -> close

§Serving

The accept loop that turns each connection into a Scope / Receive / Send and drives your moonasgi app: the Config record uvicorn's settings map onto, the lifespan protocol run around the listener, the graceful shutdown path that drains in flight requests before it closes, the reload watcher, the Date header stamped on every response, and the forwarded headers a trusted proxy is believed on.

item
async fn serve( app : @moonasgi.AsgiApp, host? : String = "127.0.0.1", port? : Int = 8000, backlog? : Int = 2048) -> Unit

Serve a moonasgi ASGI application over native HTTP/1.1 + WebSocket (← uvicorn). Convenience wrapper over serve_config that builds a Config from host / port / backlog. Blocks in a keep-alive accept loop until the running task is cancelled.

item
async fn serve_config(app : @moonasgi.AsgiApp, config : Config) -> Unit

Serve a moonasgi ASGI application under an explicit Config. Runs the full lifespan protocol around the accept loop: the app is invoked once under a Lifespan scope and startup() is driven **before** the listener is bound, and shutdown() is driven on the way out — even when the serving task is cancelled, via protect_from_cancel. Each accepted request is bridged by dispatch, which diverts WebSocket upgrades to the echo path.

struct
struct WsConfig

The WebSocket knobs uvicorn exposes on its own config (--ws-max-size). max_size is the ceiling on a received payload, in bytes: it bounds a single frame and, across continuation frames, a reassembled message, and a peer that announces more fails the connection with 1009. It is the knob with teeth on the receive side — an unbounded length is exactly what a hostile client sends. uvicorn's remaining WebSocket knobs are not here rather than here-and-ignored: --ws-ping-interval / --ws-ping-timeout need a heartbeat task alongside a reader that can be interrupted between messages and resumed, --ws-max-queue needs that same reader pumping a bounded queue, and neither is expressible while a native blocking read cannot be cancelled and resumed mid-stream; --ws-per-message-deflate needs a raw DEFLATE codec, which the async library's gzip package (gzip-framed streams only) does not provide. Accepting the extension without one would corrupt every frame.

fn
fn WsConfig::new(max_size? : Int = ws_max_size) -> WsConfig

Build a WsConfig, defaulting max_size to uvicorn's own 16 MiB.

struct
struct Config

Server configuration (← uvicorn Config): the bind address, the listen backlog, and the HTTP/1.1 transport knobs the moonbitlang/async server exposes. Constructed once and handed to serve_config. backlog mirrors uvicorn's listen(2) backlog. The current moonbitlang/async transport does not expose a backlog setter on its TCP listener — TcpServer calls listen() itself with a fixed depth and offers no way to re-listen — so the value is recorded on the config for parity and future use but is not applied to the socket. This is a transport-capability gap, not a behavioural choice, and it is the one field here that does nothing. The remaining fields map one-to-one onto knobs mooncat *does* honour: dual_stack / reuse_addr on the listening socket, server_headers stamped onto every response (uvicorn's Server: header lives here), max_connections for the parallel-client ceiling, and allow_failure for whether a handler error tears the whole server down.

fn
fn Config::new( host? : String = "127.0.0.1", port? : Int = 8000, backlog? : Int = 2048, dual_stack? : Bool = false, reuse_addr? : Bool = true, server_headers? : Map[String, String] = Map([("server", "mooncat")]), max_connections? : Int? = None, allow_failure? : Bool = true, graceful_timeout? : Int? = None, root_path? : String = "", logger? : Logger = Logger::new(), timeout_keep_alive? : Int = 5000, limit_max_requests? : Int? = None, limit_concurrency? : Int? = None, max_head_size? : Int = 16 * 1024, date_header? : Bool = true, proxy_headers? : Bool = true, forwarded_allow_ips? : Array[String] = ["127.0.0.1"], ws? : WsConfig = WsConfig::new()) -> Config

Build a Config, defaulting to uvicorn's own defaults: host 127.0.0.1, port 8000, backlog 2048, a 5-second idle keep-alive timeout, a 16 KiB request-head cap, the Date header on, proxy headers trusted from 127.0.0.1, and no request-count or concurrency limit. Transport knobs default to the async server's own defaults: reuse_addr on (uvicorn sets SO_REUSEADDR), single-stack binding, no extra response headers, an unbounded connection ceiling, and allow_failure on so a single failing handler never crashes the listener. graceful_timeout bounds how long serve_graceful waits for in-flight requests to drain before it runs lifespan shutdown anyway (← uvicorn's timeout_graceful_shutdown); None waits until the last request finishes, as uvicorn does by default.

fn
fn Config::bind(self : Config) -> String

The host:port string used to resolve the listen address.

item
suberror LifespanError

Raised when an application reports lifespan.startup.failed or lifespan.shutdown.failed, carrying the failure message the app supplied.

struct
struct Lifespan

Drives an ASGI application's lifespan protocol (← uvicorn LifespanOn). A single long-lived invocation of the app runs under a Lifespan scope. The server and that invocation exchange messages through two async queues: the server pushes lifespan.startup / lifespan.shutdown onto inbox (which backs the app's Receive), and the app pushes its *.complete / *.failed replies onto outbox (which the app's Send writes to). state seeds the lifespan scope and is shared into request scopes by the caller.

fn
fn Lifespan::new(app : @moonasgi.AsgiApp) -> Lifespan

Create a lifespan driver for app, with empty unbounded message queues and empty lifespan state.

fn
fn Lifespan::spawn( self : Lifespan, g : @async.TaskGroup[Unit]) -> @async.Task[Unit]

Spawn the application under a Lifespan scope as a task in g, returning its handle. The task blocks on receive() until startup() / shutdown() push signals; a lifespan-aware app therefore stays parked for the server's whole life, while an app that ignores the lifespan scope simply returns at once (detected via the returned task in startup / shutdown).

item
async fn Lifespan::startup( self : Lifespan, task : @async.Task[Unit]) -> Unit

Run ASGI lifespan **startup**: push lifespan.startup and await the app's reply. Returns once the app sends lifespan.startup.complete, or once the app returns without lifespan support (its task finishing wins the race, and startup is treated as a no-op, matching uvicorn's lifespan="auto"). Raises LifespanError if the app reports lifespan.startup.failed.

item
async fn Lifespan::shutdown( self : Lifespan, task : @async.Task[Unit]) -> Unit

Run ASGI lifespan **shutdown**: push lifespan.shutdown and await the app's reply, or return immediately if the app invocation has already finished. Raises LifespanError if the app reports lifespan.shutdown.failed.

struct
struct ShutdownHandle

A handle for driving a graceful shutdown of serve_graceful from outside the serving task (← uvicorn's Server.should_exit / handle_exit). Hand one to serve_graceful, then call shutdown() from any other task to stop the server: it stops accepting, waits for in-flight requests to drain, runs the ASGI lifespan shutdown, and closes the listener, in that order. Two async queues carry the handshake. request receives the shutdown trigger (a signal delivered through the runtime's global cancellation reaches the server the same way — see serve_graceful). done is posted once the server has finished the whole shutdown sequence, so shutdown() can block until the port is actually free.

fn
fn ShutdownHandle::new() -> ShutdownHandle

Create an idle shutdown handle with empty unbounded signal queues.

item
async fn ShutdownHandle::shutdown(self : ShutdownHandle) -> Unit

Request a graceful shutdown and block until the server has drained in-flight requests, run lifespan shutdown, and closed the listener. Returns once the listen port is free again.

item
async fn ShutdownHandle::request_stop(self : ShutdownHandle) -> Unit

Request a graceful shutdown without waiting for it to finish.

item
async fn serve_graceful( app : @moonasgi.AsgiApp, config : Config, handle? : ShutdownHandle = ShutdownHandle::new()) -> Unit

Serve a moonasgi application with a uvicorn-style process model: a graceful shutdown path over a single acceptor that spawns a concurrent handler per connection. The lifespan protocol runs as in serve_config — startup before the listener binds, shutdown on the way out. A single acceptor task then drives accepted connections through the same dispatch path serve uses (over a hand-built @http.ServerConnection), spawning one handler task per connection so requests are served concurrently — and, because it is the real ServerConnection, WebSocket upgrades bridge here too. Shutdown is triggered by handle.shutdown() / handle.request_stop(), by reaching Config::limit_max_requests, or by a signal the runtime turns into global cancellation (see the boundary note below). All three converge on one sequence, run under protect_from_cancel so a signal can't abort it midway: stop accepting (cancel the acceptor), drain in-flight requests (bounded by Config::graceful_timeout), run the ASGI lifespan shutdown, then close the listener. The acceptor honours the same ceilings the other serve paths get from run_forever: max_connections gates accepting, so a full server leaves the next client in the listen queue rather than accepting it and sitting on it, and allow_failure decides whether one bad connection is contained or brings the server down. limit_concurrency is the other kind of ceiling — a request arriving past it is answered 503 rather than queued. ## Multi-worker boundary uvicorn's --workers forks N OS processes that each bind the same port with SO_REUSEPORT for multi-core parallelism. moonbitlang/async exposes neither SO_REUSEPORT on TcpServer nor a fork primitive, and its event loop allows only one outstanding accept per listener handle (wait_read guards on a single waiter), so even N in-process acceptor tasks on one shared listener aren't expressible — a second concurrent accept on the same listener aborts. mooncat therefore serves from one acceptor that spawns a concurrent handler per connection, which is exactly the concurrency a single uvicorn worker provides on its single event loop. Multi-process fan-out is a transport limit, not a behavioural choice; it lands when the async layer exposes SO_REUSEPORT or a fork primitive. ## Signal boundary The only signal hook moonbitlang/async exposes is @signal.set_global_cancellation_signals, which cancels the whole task tree on SIGINT/SIGTERM. mooncat catches that cancellation and still runs lifespan shutdown and closes the listener under protect_from_cancel; but a signal also cancels the in-flight handler tasks, so drain-before-close is only fully honoured on the programmatic ShutdownHandle path. That matches uvicorn's own escalation: a first signal drains, a second forces exit.

item
async fn reload_watch(path : String, on_reload : async () -> Unit) -> Unit

Watch path for source changes and fire on_reload on each batch of events (← uvicorn's --reload file watcher). Backed by @fs.Watcher, which debounces and reports child-file events, so a save to any file under path triggers one reload. on_reload is where a supervisor re-execs the server; wiring it to a ShutdownHandle turns a file save into a graceful restart. Loops until its task is cancelled, always closing the watcher.

fn
fn http_date(ms : Int64) -> String

Format a Unix-epoch millisecond count as an HTTP-date in the IMF-fixdate form RFC 7231 §7.1.1.1 makes mandatory for a Date header: Sun, 06 Nov 1994 08:49:37 GMT, always GMT, always fixed-width. The calendar conversion is the standard civil-from-days one: shift the epoch to a March-based year so the leap day falls at the end of the era, then divide out 400-year eras. It is exact for any instant an Int64 millisecond count can name, and needs no locale and no clock but the one that produced ms.

fn
fn http_date_now() -> String

The current instant as an HTTP-date, reformatted only when the epoch second has moved on.

fn
fn proxy_rewrite( headers : Map[String, String], client : (String, Int)?, scheme : String, trusted~ : Array[String], websocket? : Bool = false) -> ((String, Int)?, String)

Rewrite a request's client and scheme from a trusted proxy's forwarding headers (← uvicorn's ProxyHeadersMiddleware). Nothing is rewritten unless the peer itself is trusted: these headers are attacker-controlled, and believing them from an arbitrary client lets that client name any address it likes for a rate limiter, an allowlist or an audit log. Behind a proxy the peer is the proxy, which is why forwarded_allow_ips defaults to 127.0.0.1 and not to *. X-Forwarded-For yields the chain's leftmost entry with port 0, since a forwarded chain carries no port. X-Forwarded-Proto replaces the scheme, mapped to wss/ws for a WebSocket scope, whose scheme names differ from the https/http the header spells.

§WebSocket

The frame-to-Event bridge that drives a websocket Scope through the SEAM: connect / accept / receive / send / disconnect / close, the RFC 6455 frame codec underneath it, and the upgrade handshake.

item
suberror WsError

A WebSocket protocol violation, carrying the RFC 6455 §7.4.1 status code the connection must be failed with (§7.1.7) and a diagnostic. Distinct from the reader's end-of-stream, which is not a violation and closes the connection without a status.

let
let ws_max_size : Int = 16 * 1024 * 1024

The default ceiling on a received payload, in bytes (← uvicorn's --ws-max-size, 16 MiB). It bounds both a single frame and a reassembled fragmented message; past it the connection is failed with 1009 rather than allocating whatever the peer asked for.

enum
enum WsOpcode

A WebSocket frame opcode (RFC 6455 §5.2): the data opcodes (continuation / text / binary) and the control opcodes (close / ping / pong).

fn
fn WsOpcode::to_int(self : WsOpcode) -> Int

The 4-bit wire value of an opcode.

fn
fn WsOpcode::is_control(self : WsOpcode) -> Bool

Whether this is a control opcode (RFC 6455 §5.5), which the spec holds to tighter rules than a data frame: at most 125 payload bytes, and never fragmented.

fn
fn ws_opcode_of_int(n : Int) -> WsOpcode?

The opcode for a 4-bit wire value, or None for a reserved opcode.

struct
struct WsFrame

A decoded WebSocket frame (RFC 6455 §5.2): its FIN bit, opcode, and already-unmasked payload.

fn
fn ws_encode_frame( fin : Bool, opcode : WsOpcode, payload : Bytes, mask? : Bytes = b"") -> Bytes

Encode a WebSocket frame (RFC 6455 §5.2). A 4-byte mask sets the MASK bit and masks the payload (clients MUST mask, servers MUST NOT — pass b"" for an unmasked server frame). The payload length takes the shortest of the 7-bit (≤125), 16-bit (126), or 64-bit (127) forms.

fn
fn ws_check_frame( b0 : Int, b1 : Int, len : Int64, from_client~ : Bool, max_size~ : Int) -> WsOpcode raise WsError

Check a frame's wire header against RFC 6455's receive rules and return its opcode. b0 and b1 are the first two header bytes; len is the payload length already widened out of whichever of the three length forms carried it. Raises WsError with the status code the connection must then be failed with (§7.1.7): * a set RSV1/2/3 bit, no extension having been negotiated → 1002 (§5.2); * a reserved opcode → 1002 (§5.2); * an unmasked frame, when from_client says the peer is a client → 1002 (§5.1); * a control frame carrying more than 125 bytes, or fragmented → 1002 (§5.5); * a length past max_size → 1009, the 64-bit form's sign bit included (§5.2 reserves it). Callers must run this *before* they read the payload: it is the length check that keeps a 64-bit length from being truncated into a short allocation, and keeps an honest-but-enormous one from being allocated at all.

fn
fn ws_decode_frame( b : BytesView, from_client? : Bool = true, max_size? : Int = ws_max_size) -> (WsFrame, Int)? raise WsError

Decode one WebSocket frame at the start of b (RFC 6455 §5.2), returning the frame (its payload unmasked) and the byte count it consumed, or None when b does not yet hold a whole frame. from_client selects the receive rules of a server reading a client (the default: masking is mandatory); pass false to decode a frame travelling the other way. A frame that breaks the rules raises WsError rather than returning None, so a violation is never mistaken for a short buffer — see ws_check_frame.

item
async fn ws_read_frame( reader : &@io.Reader, from_client? : Bool = true, max_size? : Int = ws_max_size) -> WsFrame

Read one WebSocket frame off a raw byte stream (RFC 6455 §5.2) — the runtime counterpart of ws_decode_frame. Reads the 2-byte prefix, then the extended length (16- or 64-bit), and only once ws_check_frame has passed the header does it read the masking key and the payload. from_client defaults to the server's position — a client's frames must be masked (§5.1) — so reading a frame the server sent needs from_client=false. Raises WsError carrying the close code for any violation, and propagates the reader's end-of-stream.

enum
enum WsMessage

A complete WebSocket application message read off a raw stream: a text or binary message with its reassembled payload, or a close with its code and reason bytes. Control frames never surface here — ws_read_message handles them — matching what uvicorn hands the ASGI app. A text message carries a String: §8.1 makes invalid UTF-8 a 1007 failure, so bytes that would not decode never reach this far and the app cannot be handed a lossily-mangled message.

fn
fn ws_close_payload(code : Int, reason : String) -> Bytes

Build a close frame's payload (RFC 6455 §5.5.1): the 16-bit status code big-endian followed by the UTF-8 reason.

item
async fn ws_read_message( reader : &@io.Reader, writer : &@io.Writer, max_size? : Int = ws_max_size) -> WsMessage

Read one complete WebSocket message off reader, reassembling a fragmented message from its continuation frames (RFC 6455 §5.4) and handling interleaved control frames (§5.5): a ping is answered on writer with a pong, a pong is dropped, and a close is echoed back per §5.5.1 before the message ends with its code and reason. This is the server's receive path, so every frame must arrive masked. Raises WsError carrying the close code the connection is then failed with: 1002 for a framing violation (a stray continuation frame, or a new data frame arriving before the current message finished) and whatever ws_check_frame decided for a bad header, 1007 for a text message that is not valid UTF-8 (§8.1), 1009 for a reassembled message past max_size.

item
extend WsFrame with Debug::
item
extend WsFrame with Eq::
item
extend WsMessage with Debug::
item
extend WsMessage with Eq::
item
extend WsOpcode with Debug::
item
extend WsOpcode with Eq::
let
let websocket_guid : String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

The WebSocket handshake GUID appended to the client key before hashing (RFC 6455 §4.2.2).

fn
fn websocket_accept_key(sec_websocket_key : String) -> String

The Sec-WebSocket-Accept value for a client's Sec-WebSocket-Key: base64 of the SHA-1 of the key concatenated with the GUID (RFC 6455 §4.2.2).

fn
fn websocket_select_subprotocol( offered : Array[String], supported : Array[String]) -> String?

Select the first client-offered subprotocol the server supports, or None if none match (RFC 6455 §4.2.2): the client's order is its preference.

fn
fn websocket_handshake_response( sec_websocket_key : String, subprotocol : String?, extra_headers : Array[(String, String)]) -> Bytes

The 101 Switching Protocols response for a WebSocket upgrade (RFC 6455 §4.2.2): the Upgrade and Connection headers, the computed Sec-WebSocket-Accept, the selected subprotocol when one was negotiated, and any extra_headers.

§HTTP/2 (h2c)

Serving HTTP/2 over cleartext: the connection preface, frame pump and stream multiplexing, over the same dispatch path HTTP/1.1 uses.

item
async fn serve_h2c( app : @moonasgi.AsgiApp, host? : String = "127.0.0.1", port? : Int = 8000) -> Unit

Serve a moonasgi ASGI application over **HTTP/2 cleartext (h2c)** — the prior-knowledge, no-TLS HTTP/2 profile (RFC 7540 §3.4) — reusing moonrpc's self-built HTTP/2 + HPACK engine as the transport. Convenience wrapper over serve_h2c_config that builds a Config from host / port. Blocks in the accept loop until the running task is cancelled. h2c rather than h2-over-TLS because the moonbitlang/async TLS layer exposes no ALPN, so the protocol can't be negotiated on a TLS connection yet; h2c is the direct, ALPN-free path a client reaches with prior knowledge (as curl --http2-prior-knowledge or a gRPC client does).

item
async fn serve_h2c_config(app : @moonasgi.AsgiApp, config : Config) -> Unit

Serve a moonasgi ASGI application over h2c under an explicit Config. Mirrors serve_config / serve_tls_config: the ASGI lifespan protocol runs around the accept loop (startup before the listener binds, shutdown on the way out — even under cancellation, guarded by protect_from_cancel), and each accepted connection is driven by drive_h2c over the self-built HTTP/2 transport.

§TLS 1.3

A TLS 1.3 stack written here rather than bound: the client and server state machines, key schedule and transcript, ECDHE key share, ALPN, certificate and CertificateVerify, the QUIC transport-parameters extension, the mandatory hello extensions and the negotiation they drive, HelloRetryRequest, the alert protocol, the record layer that carries all of it over TCP, plus the X.509 and DER parsing they rest on.

fn
fn tls13_transcript_hash(messages : Bytes) -> Bytes

The transcript hash over a run of handshake messages (RFC 8446 §4.4.1): here the SHA-256 of their concatenation.

fn
fn tls13_derive_secret( secret : Bytes, label : Bytes, transcript_hash : Bytes) -> Bytes

Derive-Secret(Secret, Label, Messages) (RFC 8446 §7.1): HKDF-Expand-Label with the transcript hash as context and the hash length as output. transcript_hash is the Transcript-Hash of the messages (use tls13_transcript_hash, or the empty-string hash for the "derived" steps).

fn
fn tls13_early_secret(psk : Bytes) -> Bytes

The Early Secret: HKDF-Extract(0, PSK), with an all-zero PSK when none is used.

fn
fn tls13_handshake_secret(early_secret : Bytes, ecdhe : Bytes) -> Bytes

The Handshake Secret: HKDF-Extract over the ECDHE shared secret, salted by the Early Secret run through the "derived" step (RFC 8446 §7.1).

fn
fn tls13_master_secret(handshake_secret : Bytes) -> Bytes

The Master Secret: HKDF-Extract over an all-zero IKM, salted by the Handshake Secret run through the "derived" step (RFC 8446 §7.1).

fn
fn tls13_finished_key(base_key : Bytes) -> Bytes

A Finished key: HKDF-Expand-Label(BaseKey, "finished", "", Hash.length) (RFC 8446 §4.4.4), the key that MACs the Finished message's verify_data.

let
let tls_alert_level_warning : Int = 1

The warning alert level (RFC 8446 §6). TLS 1.3 acts on the description rather than the level: only close_notify and user_canceled leave the connection usable, whichever level carries them.

let
let tls_alert_level_fatal : Int = 2

The fatal alert level (RFC 8446 §6): the sender closes the connection immediately and discards the keys.

let
let tls_alert_close_notify : Int = 0

Alert descriptions (RFC 8446 §6.1, §6.2). Only the codes the RFC assigns are named; an unrecognized description on the wire is still carried through as its number. The sender is done writing — a clean shutdown, not a failure.

let
let tls_alert_unexpected_message : Int = 10

A message arrived that the state machine was not waiting for, or out of order.

let
let tls_alert_bad_record_mac : Int = 20

A record failed to decrypt or to authenticate.

let
let tls_alert_record_overflow : Int = 22

A record's plaintext ran past the 2^14-octet limit.

let
let tls_alert_handshake_failure : Int = 40

No parameters both endpoints support — no shared group, cipher suite, or signature scheme.

let
let tls_alert_bad_certificate : Int = 42

A certificate was corrupt, or carried a signature that did not verify.

let
let tls_alert_unsupported_certificate : Int = 43

The certificate was of a type the receiver does not support.

let
let tls_alert_certificate_revoked : Int = 44

The signer revoked the certificate.

let
let tls_alert_certificate_expired : Int = 45

The certificate has expired, or is not yet valid.

let
let tls_alert_certificate_unknown : Int = 46

Some other problem with the certificate, with no more precise description to give.

let
let tls_alert_illegal_parameter : Int = 47

A field was well-formed on its own but its value contradicts the rest of the message.

let
let tls_alert_unknown_ca : Int = 48

The chain verified but ended at no trusted anchor.

let
let tls_alert_access_denied : Int = 49

The certificate verified, but the receiver declines to proceed with this peer.

let
let tls_alert_decode_error : Int = 50

A message could not be parsed: a length overran what was there, or a field was out of range.

let
let tls_alert_decrypt_error : Int = 51

A handshake signature or a Finished MAC failed to verify.

let
let tls_alert_protocol_version : Int = 70

The peer offered no protocol version this endpoint supports.

let
let tls_alert_insufficient_security : Int = 71

handshake_failure, narrowed: the peer's parameters are weaker than this endpoint accepts.

let
let tls_alert_internal_error : Int = 80

A local failure, unrelated to the peer or to the correctness of the protocol.

let
let tls_alert_inappropriate_fallback : Int = 86

The peer fell back to an older version for a reason this endpoint does not accept.

let
let tls_alert_user_canceled : Int = 90

The handshake is being abandoned for a reason outside the protocol.

let
let tls_alert_missing_extension : Int = 109

A message lacked an extension RFC 8446 §9.2 makes mandatory for it.

let
let tls_alert_unsupported_extension : Int = 110

A server sent back an extension the ClientHello never offered.

let
let tls_alert_unrecognized_name : Int = 112

No server exists for the name the client's server_name extension asked for.

let
let tls_alert_bad_certificate_status_response : Int = 113

The stapled OCSP response was invalid.

let
let tls_alert_unknown_psk_identity : Int = 115

The PSK identity the client offered is unknown and no full handshake will be run instead.

let
let tls_alert_certificate_required : Int = 116

The client sent no certificate where the server requires one.

let
let tls_alert_no_application_protocol : Int = 120

None of the ALPN protocols the client offered is one this server speaks (RFC 7301 §3.2).

item
suberror TlsAlert

An alert (RFC 8446 §6). It is the error the handshake raises, so a caller catches the reason and the two bytes to send in one value.

fn
fn tls_fatal(description : Int) -> TlsAlert

A fatal alert carrying description — every way a TLS 1.3 handshake can fail.

fn
fn tls_warning(description : Int) -> TlsAlert

A warning alert carrying description: close_notify and user_canceled, the two the peer may act on without tearing the connection down.

fn
fn tls_encode_alert(alert : TlsAlert) -> Bytes

The two bytes an alert goes on the wire as (RFC 8446 §6).

fn
fn tls_decode_alert(view : BytesView) -> TlsAlert?

Read an alert back off the wire, or None if fewer than two bytes are there.

fn
fn tls_alert_is_fatal(alert : TlsAlert) -> Bool

Whether an alert ends the connection (RFC 8446 §6.1): everything but close_notify and user_canceled, whatever level the sender put on it.

item
extend TlsAlert with Debug::
item
extend TlsAlert with Eq::
let
let tls_ext_alpn : Int = 0x0010

The application_layer_protocol_negotiation extension type (RFC 7301 §3.1).

let
let alpn_h3 : String = "h3"

The HTTP/3 ALPN protocol identifier (RFC 9114 §3.1).

fn
fn tls_encode_alpn(protocols : Array[String]) -> Bytes

Encode an ALPN ProtocolNameList (RFC 7301 §3.1): a two-byte length of the name list, then each protocol as a one-byte length and its bytes.

fn
fn tls_decode_alpn(view : BytesView) -> Array[String]

Decode an ALPN ProtocolNameList back into its protocol names, stopping at the declared list length or a truncated entry.

fn
fn tls_alpn_extension(protocols : Array[String]) -> TlsExtension

Build an ALPN extension carrying protocols — the offered list in a ClientHello, or the single selected protocol in a server's reply.

fn
fn tls_client_hello_alpn(extensions : Array[TlsExtension]) -> Array[String]

The protocols a ClientHello's ALPN extension offers, in order (empty if it has none).

fn
fn tls_selected_alpn(extensions : Array[TlsExtension]) -> String?

The single protocol a server's ALPN extension selected, or None if there is none.

fn
fn tls13_encode_certificate( certificate_request_context : Bytes, certs : Array[Bytes]) -> Bytes

Encode a Certificate message body (RFC 8446 §4.4.2): the certificate_request_context (empty for a server certificate sent without a CertificateRequest), then the certificate_list — each entry a three-byte-length DER certificate and a two-byte-length extensions block (empty here).

fn
fn tls13_decode_certificate(body : BytesView) -> (Bytes, Array[Bytes])?

Decode a Certificate message body into its certificate_request_context and the list of DER certificates (RFC 8446 §4.4.2), skipping per-entry extensions. None on a truncated message.

let
let tls_sig_ecdsa_secp256r1_sha256 : Int = 0x0403

The ecdsa_secp256r1_sha256 (ES256) SignatureScheme (RFC 8446 §4.2.3).

let
let tls13_cv_context_server : String = "TLS 1.3, server CertificateVerify"

The server's CertificateVerify context string (RFC 8446 §4.4.3).

let
let tls13_cv_context_client : String = "TLS 1.3, client CertificateVerify"

The client's CertificateVerify context string (RFC 8446 §4.4.3).

fn
fn tls13_certificate_verify_content( context : String, transcript_hash : Bytes) -> Bytes

The content a CertificateVerify signs (RFC 8446 §4.4.3): 64 octets of 0x20, the context string, a single 0x00 separator, then the transcript_hash through the Certificate.

fn
fn tls13_certificate_verify_sign( key : EcdsaPrivateKey, context : String, transcript_hash : Bytes) -> Bytes

Sign a CertificateVerify over the transcript with an ES256 key (RFC 8446 §4.4.3): returns the message body — the SignatureScheme (ecdsa_secp256r1_sha256) and the two-byte-length- prefixed raw r||s signature over the §4.4.3 signed content.

fn
fn tls13_certificate_verify_check( key : EcdsaPublicKey, context : String, transcript_hash : Bytes, body : Bytes) -> Bool

Verify a CertificateVerify message body against the peer's ES256 public key over the transcript (RFC 8446 §4.4.3): parse the SignatureScheme and signature, then check the ECDSA signature over the §4.4.3 signed content. False on a wrong scheme, a malformed body, or an invalid signature.

enum
enum TlsClientState

A client's handshake state (RFC 8446 A.1), after the ClientHello has been sent.

fn
fn TlsClientState::new() -> TlsClientState

The initial state, having just sent the ClientHello (START -> WAIT_SH; sending the ClientHello is the client's output action, not a received message).

fn
fn tls_client_next( state : TlsClientState, msg_type : Int) -> TlsClientState raise TlsAlert

The next client state on receiving a handshake message of msg_type (RFC 8446 A.1). A message that does not belong in the current state raises the unexpected_message alert (§6.2).

item
extend TlsClientState with Debug::
item
extend TlsClientState with Eq::
fn
fn tls13_ecdhe_shared(private_key : Bytes, peer_public : Bytes) -> Bytes

The x25519 ECDHE shared secret from an endpoint's private key and the peer's public key.

fn
fn tls13_handshake_secret_from_ecdhe( private_key : Bytes, peer_public : Bytes) -> Bytes

The Handshake Secret for an x25519 ECDHE exchange with no PSK (RFC 8446 §7.1): the key schedule extracts the shared secret under the Early Secret's derived salt.

fn
fn tls13_ecdhe_handshake_traffic_secret( private_key : Bytes, peer_public : Bytes, transcript_hash : Bytes, is_client : Bool) -> Bytes

An endpoint's handshake traffic secret from the ECDHE exchange and the ClientHello.. ServerHello transcript hash (RFC 8446 §7.1): the client secret when is_client, otherwise the server secret. Both peers derive matching secrets because they share the same DHE.

fn
fn tls13_server_hs_secret_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> Bytes?

The server's handshake traffic secret derived from a received ClientHello message alone: pull the client's x25519 key_share and run the ECDHE with the server's ephemeral server_private over the ClientHello..ServerHello transcript_hash. None if the message is not a ClientHello or offers no x25519 share.

fn
fn tls13_ecdhe_secrets_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> (Bytes, Bytes)?

Both handshake traffic secrets a server derives from a received ClientHello and its own ephemeral private key (RFC 8446 §7.1), as (server_secret, client_secret): pull the client's x25519 key_share, run the ECDHE with server_private over the ClientHello.. ServerHello transcript_hash, and derive each side's secret. None if the message is not a ClientHello or offers no x25519 share. A server protects its own flight with the server secret and verifies the client Finished with the client secret — deriving both from the wire ClientHello, never from the peer's copy.

fn
fn tls13_encrypted_extensions_body(exts : Array[TlsExtension]) -> Bytes

The EncryptedExtensions message body (RFC 8446 §4.3.1): an extension list.

fn
fn tls13_decode_encrypted_extensions( body : BytesView) -> Array[TlsExtension]?

Decode an EncryptedExtensions body back into its extension list, or None if truncated.

fn
fn tls13_quic_encrypted_extensions( alpn : String, params : Array[TransportParam]) -> Bytes

A QUIC server's EncryptedExtensions body: the negotiated ALPN protocol and the server's transport parameters (RFC 9001 §8.2 — a QUIC server MUST send quic_transport_parameters).

struct
struct TlsServerHandshake

A server-side TLS 1.3 handshake in progress.

fn
fn TlsServerHandshake::new() -> TlsServerHandshake

A fresh handshake, awaiting the ClientHello.

fn
fn TlsServerHandshake::state(self : TlsServerHandshake) -> TlsServerState

The current handshake state.

fn
fn TlsServerHandshake::transcript_hash(self : TlsServerHandshake) -> Bytes

The running transcript hash over every message seen so far, in order.

fn
fn TlsServerHandshake::is_connected(self : TlsServerHandshake) -> Bool

Whether the handshake has completed.

fn
fn TlsServerHandshake::client_hello(self : TlsServerHandshake) -> Bytes

The raw ClientHello message the handshake received (empty until one arrives). A server needs it to pull the client's key_share and run the ECDHE that derives the handshake secrets.

fn
fn TlsServerHandshake::negotiate( self : TlsServerHandshake) -> TlsNegotiation raise TlsAlert

Negotiate the ClientHello this handshake received (RFC 8446 §4.1.1): the group, the client's key share, and the signature scheme its CertificateVerify will use, or a Retry when the group has no share yet. Raises the §6 alert for a ClientHello this build cannot serve — decode_error before one has arrived at all.

fn
fn TlsServerHandshake::feed( self : TlsServerHandshake, bytes : Bytes) -> Array[Int] raise

Feed received CRYPTO bytes: process every complete handshake message now available — add it to the transcript and drive the state machine — buffering any trailing partial message. Returns the handshake types processed, in order.

fn
fn TlsServerHandshake::record_sent( self : TlsServerHandshake, message : Bytes) -> Unit

Fold a handshake message the server sends (ServerHello, EncryptedExtensions, Certificate, ...) into the transcript, keeping it in message order.

fn
fn TlsServerHandshake::sent_flight( self : TlsServerHandshake, request_client_cert : Bool) -> Unit raise

Advance the state machine past the server's own flight (after the ClientHello), waiting for a client certificate when request_client_cert is set.

let
let tls_ext_supported_groups : Int = 0x000a

The supported_groups extension type (RFC 8446 §4.2.7).

let
let tls_ext_signature_algorithms : Int = 0x000d

The signature_algorithms extension type (RFC 8446 §4.2.3).

let
let tls_ext_supported_versions : Int = 0x002b

The supported_versions extension type (RFC 8446 §4.2.1).

let
let tls_legacy_version : Int = 0x0303

The version TLS 1.3 leaves in the legacy_version field of a ClientHello and ServerHello (RFC 8446 §4.1.2, §4.1.3): TLS 1.2's number, kept so middleboxes pass the record through.

let
let tls_version_13 : Int = 0x0304

TLS 1.3's version number (RFC 8446 §4.2.1), which travels in supported_versions rather than in legacy_version.

let
let tls_group_secp256r1 : Int = 0x0017

The secp256r1 named group (RFC 8446 §4.2.7).

let
let tls_group_secp384r1 : Int = 0x0018

The secp384r1 named group (RFC 8446 §4.2.7).

let
let tls_sig_rsa_pss_rsae_sha256 : Int = 0x0804

The rsa_pss_rsae_sha256 SignatureScheme (RFC 8446 §4.2.3).

fn
fn tls_encode_supported_versions_client(versions : Array[Int]) -> Bytes

Encode a ClientHello's supported_versions body (RFC 8446 §4.2.1): a one-byte length, then each version as two bytes, most preferred first.

fn
fn tls_decode_supported_versions_client(view : BytesView) -> Array[Int]

Decode a ClientHello's supported_versions body into the versions it offers, in order.

fn
fn tls_encode_supported_versions_server(version : Int) -> Bytes

Encode a ServerHello's supported_versions body (RFC 8446 §4.2.1): the selected version alone, with no list prefix.

fn
fn tls_decode_supported_versions_server(view : BytesView) -> Int?

The version a ServerHello's supported_versions body selected, or None if it is not two bytes.

fn
fn tls_supported_versions_extension(versions : Array[Int]) -> TlsExtension

A ClientHello supported_versions extension offering versions.

fn
fn tls_selected_version_extension(version : Int) -> TlsExtension

A ServerHello or HelloRetryRequest supported_versions extension naming version.

fn
fn tls_client_hello_versions( extensions : Array[TlsExtension]) -> Array[Int]

The versions a ClientHello's supported_versions extension offers (empty if it has none).

fn
fn tls_server_hello_version(sh : TlsServerHello) -> Int?

The version a ServerHello selected (RFC 8446 §4.2.1) — the field a client must read instead of legacy_version — or None if it carries no supported_versions.

fn
fn tls_encode_supported_groups(groups : Array[Int]) -> Bytes

Encode a supported_groups body (RFC 8446 §4.2.7): a two-byte list length, then each named group as two bytes, most preferred first.

fn
fn tls_decode_supported_groups(view : BytesView) -> Array[Int]

Decode a supported_groups body into the named groups it offers, in order.

fn
fn tls_supported_groups_extension(groups : Array[Int]) -> TlsExtension

A supported_groups extension offering groups.

fn
fn tls_client_hello_groups(extensions : Array[TlsExtension]) -> Array[Int]

The named groups a ClientHello offers for key exchange (empty if it has no supported_groups).

fn
fn tls_encode_signature_algorithms(schemes : Array[Int]) -> Bytes

Encode a signature_algorithms body (RFC 8446 §4.2.3): a two-byte list length, then each SignatureScheme as two bytes, most preferred first.

fn
fn tls_decode_signature_algorithms(view : BytesView) -> Array[Int]

Decode a signature_algorithms body into the schemes it offers, in order.

fn
fn tls_signature_algorithms_extension(schemes : Array[Int]) -> TlsExtension

A signature_algorithms extension offering schemes.

fn
fn tls_client_hello_signature_algorithms( extensions : Array[TlsExtension]) -> Array[Int]

The signature schemes a ClientHello will accept on a certificate (empty if it has no signature_algorithms).

fn
fn tls13_server_hello( random : Bytes, session_id : Bytes, cipher_suite : Int, extensions : Array[TlsExtension]) -> TlsServerHello

A ServerHello carrying the supported_versions RFC 8446 §9.2 requires, ahead of extensions. The message's legacy_version stays 0x0303 — encode_server_hello writes it — because §4.1.3 puts the negotiated version here instead.

fn
fn tls13_groups() -> Array[Int]

The named groups this build can run a key exchange for. x25519.mbt is the only group implemented, so offering more would be a lie the handshake could not honour.

fn
fn tls13_signature_schemes() -> Array[Int]

The signature schemes this build can sign a CertificateVerify with. ecdsa.mbt implements P-256 with SHA-256 and nothing else.

enum
enum TlsNegotiation

What a server settled on after reading a ClientHello.

fn
fn tls13_negotiate(ch : TlsClientHello) -> TlsNegotiation raise TlsAlert

Negotiate a decoded ClientHello (RFC 8446 §4.1.1): check it offers TLS 1.3, pick the first group and signature scheme it lists that this build runs, and take its key share for the chosen group. Raises the alert §6 names for each way that fails — a missing mandatory extension, a client that does not speak 1.3, nothing in common, or a key share that contradicts the rest of the message — and returns Retry when the chosen group is one the client offered but sent no share for.

fn
fn tls13_negotiate_client_hello( message : Bytes) -> TlsNegotiation raise TlsAlert

Negotiate a raw ClientHello handshake message: frame it, decode it, and run tls13_negotiate. A message that is not a well-formed ClientHello raises decode_error (RFC 8446 §6.2) rather than vanishing into a None.

item
extend TlsNegotiation with Debug::
item
extend TlsNegotiation with Eq::
let
let tls13_hello_retry_random : Bytes = b"\xcf\x21\xad\x74\xe5\x9a\x61\x11\xbe\x1d\x8c\x02\x1e\x65\xb8\x91\xc2\xa2\x11\x16\x7a\xbb\x8c\x5e\x07\x9e\x09\xe2\xc8\xa8\x33\x9c"

The ServerHello.random that marks a HelloRetryRequest (RFC 8446 §4.1.3): the SHA-256 of "HelloRetryRequest". A receiver must compare against it before reading the message as a real ServerHello.

fn
fn tls_encode_key_share_retry(group : Int) -> Bytes

Encode the key_share body of a HelloRetryRequest (RFC 8446 §4.2.8): the selected group alone. There is no key — asking for one is the whole point of the message.

fn
fn tls_decode_key_share_retry(view : BytesView) -> Int?

The group a HelloRetryRequest's key_share body names, or None if it is not two bytes.

fn
fn tls_key_share_retry_extension(group : Int) -> TlsExtension

A HelloRetryRequest key_share extension naming group.

fn
fn tls13_hello_retry_request( session_id : Bytes, cipher_suite : Int, group : Int) -> TlsServerHello

A HelloRetryRequest asking the client to come back with a key share for group (RFC 8446 §4.1.4): the sentinel random, the client's session_id echoed back untouched, the chosen cipher_suite, supported_versions, and the group. It encodes through encode_server_hello like any other ServerHello.

fn
fn tls13_is_hello_retry_request(sh : TlsServerHello) -> Bool

Whether a ServerHello is really a HelloRetryRequest (RFC 8446 §4.1.3).

fn
fn tls13_hello_retry_group(sh : TlsServerHello) -> Int?

The group a HelloRetryRequest asks for a share of, or None if the message is an ordinary ServerHello or carries no key_share.

let
let tls_group_x25519 : Int = 0x001d

The x25519 named group (RFC 8446 §4.2.7).

let
let tls_ext_key_share : Int = 0x0033

The key_share extension type (RFC 8446 §4.2.8).

fn
fn tls_server_hello_key_share(sh : TlsServerHello) -> (Int, Bytes)?

The (group, key) of a ServerHello's key_share extension, or None if it has none.

fn
fn tls_client_hello_key_shares( extensions : Array[TlsExtension]) -> Array[(Int, Bytes)]

The list of (group, key) shares a ClientHello's key_share extension offers.

fn
fn tls_key_share_extension(group : Int, key : Bytes) -> TlsExtension

Build a ServerHello key_share extension carrying key for group.

fn
fn tls13_x25519_public(private_key : Bytes) -> Bytes

The ephemeral x25519 public key for a private scalar: x25519(private, base_point).

fn
fn tls_encode_key_share_entry(group : Int, key : Bytes) -> Bytes

Encode one KeyShareEntry: the named group, the key length, then the key (RFC 8446 §4.2.8).

fn
fn tls_encode_key_share_server(group : Int, key : Bytes) -> Bytes

Encode the ServerHello key_share extension body: a single KeyShareEntry.

fn
fn tls_encode_key_share_client(entries : Array[(Int, Bytes)]) -> Bytes

Encode the ClientHello key_share extension body: the client_shares length, then the KeyShareEntry list.

fn
fn tls_decode_key_share_entry(view : BytesView) -> (Int, Bytes, Int)?

Decode one KeyShareEntry from the front of view: returns (group, key, bytes-consumed), or None on a partial read.

fn
fn tls_decode_key_share_server(view : BytesView) -> (Int, Bytes)?

Decode a ServerHello key_share extension body: its single KeyShareEntry as (group, key).

fn
fn tls_decode_key_share_client(view : BytesView) -> Array[(Int, Bytes)]

Decode a ClientHello key_share extension body: the list of (group, key) shares.

let
let tls_ext_quic_transport_params : Int = 0x0039

The quic_transport_parameters extension type (RFC 9001 §8.2).

fn
fn tls_quic_transport_params_extension( params : Array[TransportParam]) -> TlsExtension

Build a quic_transport_parameters extension carrying params — the client's block in a ClientHello, the server's in its EncryptedExtensions.

fn
fn tls_hello_quic_transport_params( extensions : Array[TlsExtension]) -> Array[TransportParam]

The QUIC transport parameters an extension list carries, in order (empty if the extension is absent or its block is truncated).

let
let tls_record_change_cipher_spec : Int = 20

The change_cipher_spec content type (RFC 8446 §5). TLS 1.3 has no such message: the record survives only so middleboxes watching for a TLS 1.2 handshake see one.

let
let tls_record_alert : Int = 21

The alert content type (RFC 8446 §5).

let
let tls_record_handshake : Int = 22

The handshake content type (RFC 8446 §5).

let
let tls_record_application_data : Int = 23

The application_data content type (RFC 8446 §5). Every TLSCiphertext carries it as the outer opaque_type too, whatever the real type sealed inside turns out to be.

let
let tls_record_max_fragment : Int = 16384

The largest TLSPlaintext fragment (RFC 8446 §5.1): 2^14 octets.

let
let tls_record_max_encrypted : Int = 16640

The largest TLSCiphertext encrypted_record (RFC 8446 §5.2): 2^14 + 256 — the fragment limit plus the room the inner content type, the padding and the AEAD tag need.

let
let tls_record_header_len : Int = 5

The record header: content type, legacy_record_version, and a 16-bit length.

struct
struct TlsRecord

One record's content type and fragment (RFC 8446 §5.1). For a TLSCiphertext the content_type is the outer application_data and the fragment is still sealed.

fn
fn tls_record_encode( content_type : Int, fragment : Bytes) -> Bytes raise TlsAlert

Frame fragment as a TLSPlaintext record (RFC 8446 §5.1). legacy_record_version is 0x0303 whatever the negotiated version is; a receiver ignores it.

fn
fn tls_record_parse(view : BytesView) -> (TlsRecord, Int)? raise TlsAlert

Parse one TLSPlaintext record (RFC 8446 §5.1) and the number of bytes it consumed, or None while the view is short of a whole record.

fn
fn tls_record_parse_encrypted( view : BytesView) -> (TlsRecord, Int)? raise TlsAlert

Parse one TLSCiphertext record (RFC 8446 §5.2) and the number of bytes it consumed. The fragment is the still-sealed encrypted_record, so the ceiling is the larger §5.2 one.

fn
fn tls_record_inner( content_type : Int, content : Bytes, padding : Int) -> Bytes

A TLSInnerPlaintext (RFC 8446 §5.2): the content, then the content type the outer header is hiding, then padding zero octets. The padding is the §5.4 length-hiding knob.

fn
fn tls_record_unpad(inner : BytesView) -> (Int, Bytes) raise TlsAlert

Split a TLSInnerPlaintext back into its content type and content (RFC 8446 §5.2): scan back over the zero padding to the last non-zero octet, which is the type. An inner plaintext of nothing but zeros names no type at all, which §5.2 makes an unexpected_message rather than an empty record.

struct
struct TlsTrafficKeys

The AEAD key and IV one direction protects its records with (RFC 8446 §7.3), at the TLS_AES_128_GCM_SHA256 lengths.

fn
fn tls13_traffic_keys(secret : Bytes) -> TlsTrafficKeys

Derive a direction's record-protection key and IV from its traffic secret (RFC 8446 §7.3). QUIC expands the same secret under "quic key"/"quic iv" and adds a header-protection key (RFC 9001 §5.1); over TCP the labels are bare and there is no header to protect.

fn
fn tls_record_nonce(iv : Bytes, seq : UInt64) -> Bytes

The per-record AEAD nonce (RFC 8446 §5.3): the 64-bit sequence number, big-endian and left-padded with zeros to the IV's length, XORed with the static write IV. The IV never changes, so the sequence number alone is what keeps two records under one key from ever sharing a nonce.

struct
struct TlsRecordKeys

One direction's record protection: its traffic keys, and the sequence number that keeps their nonces apart. RFC 8446 §5.3 restarts the sequence at zero on every key change, which is what rekey is for — the IV is reused across a generation, so a sequence that carried over would reuse a nonce under the new key.

fn
fn TlsRecordKeys::new(secret : Bytes) -> TlsRecordKeys

Record protection under the traffic secret, starting at sequence zero.

fn
fn TlsRecordKeys::rekey(self : TlsRecordKeys, secret : Bytes) -> Unit

Install a new traffic secret and restart the sequence number at zero (RFC 8446 §5.3).

fn
fn TlsRecordKeys::keys(self : TlsRecordKeys) -> TlsTrafficKeys

The traffic keys in force.

fn
fn TlsRecordKeys::seq(self : TlsRecordKeys) -> UInt64

The sequence number the next record will use.

fn
fn TlsRecordKeys::nonce(self : TlsRecordKeys) -> Bytes

The nonce the next record will be sealed or opened under (RFC 8446 §5.3).

fn
fn TlsRecordKeys::seal_padded( self : TlsRecordKeys, content_type : Int, content : Bytes, padding : Int) -> Bytes raise TlsAlert

Seal content as a TLSCiphertext record (RFC 8446 §5.2) with padding zero octets after the inner content type, and advance the sequence number.

fn
fn TlsRecordKeys::seal( self : TlsRecordKeys, content_type : Int, content : Bytes) -> Bytes raise TlsAlert

Seal content as a TLSCiphertext record with no padding.

fn
fn TlsRecordKeys::open( self : TlsRecordKeys, record : BytesView) -> (Int, Bytes) raise TlsAlert

Open one whole TLSCiphertext record — header included, since RFC 8446 §5.2 makes that header the AEAD's additional data — into the content type and content sealed inside, advancing the sequence number. A tag that does not verify is a bad_record_mac and the sequence does not move, because §5.2 ends the connection there.

item
extend TlsRecord with Debug::
item
extend TlsRecord with Eq::
item
extend TlsTrafficKeys with Debug::
item
extend TlsTrafficKeys with Eq::
enum
enum TlsRecordEvent

What a record turned into. A HandshakeMsg carries the whole message, header included, so it can go straight into the transcript hash as well as be parsed.

struct
struct TlsRecordLayer

A record layer in progress: what has arrived but not yet parsed, the handshake bytes still short of a whole message, and each direction's keys once they exist.

fn
fn TlsRecordLayer::new() -> TlsRecordLayer

A record layer with no keys yet: everything is TLSPlaintext, as it is for the ClientHello and ServerHello.

fn
fn TlsRecordLayer::set_read_secret( self : TlsRecordLayer, secret : Bytes) -> Unit

Install the read traffic secret: records from the peer are TLSCiphertext from here on and the receive sequence restarts at zero (RFC 8446 §5.3).

fn
fn TlsRecordLayer::set_write_secret( self : TlsRecordLayer, secret : Bytes) -> Unit

Install the write traffic secret: records this endpoint sends are TLSCiphertext from here on and the send sequence restarts at zero (RFC 8446 §5.3).

fn
fn TlsRecordLayer::read_seq(self : TlsRecordLayer) -> UInt64

The sequence number the next record read will use, zero while no read keys are in.

fn
fn TlsRecordLayer::write_seq(self : TlsRecordLayer) -> UInt64

The sequence number the next record written will use, zero while no write keys are in.

fn
fn TlsRecordLayer::handshake_done(self : TlsRecordLayer) -> Unit

Close the middlebox-compatibility window (RFC 8446 §5): once the handshake is finished a change_cipher_spec has nothing left to pretend to be, and the next one is an unexpected_message.

fn
fn TlsRecordLayer::has_partial(self : TlsRecordLayer) -> Bool

Whether a handshake message has arrived in part and is waiting on the rest of itself.

fn
fn TlsRecordLayer::feed( self : TlsRecordLayer, bytes : Bytes) -> Array[TlsRecordEvent] raise TlsAlert

Feed received bytes and take out everything now complete, in wire order. Bytes short of a whole record are held for the next call. A record that breaks §5 — an undefined content type, a length past the limit, a change_cipher_spec outside its window, a tag that does not verify — raises the alert §5 names for it, and the connection is over.

fn
fn TlsRecordLayer::write_handshake( self : TlsRecordLayer, messages : Bytes) -> Bytes raise TlsAlert

The wire bytes for one or more handshake messages. Pass a whole flight to coalesce it into one record; a message past the fragment limit is split across records by itself (RFC 8446 §5.1), which handshake content is the only content allowed to do.

fn
fn TlsRecordLayer::write_app( self : TlsRecordLayer, data : Bytes) -> Bytes raise TlsAlert

The wire bytes for application data, split across records at the fragment limit.

fn
fn TlsRecordLayer::write_alert( self : TlsRecordLayer, alert : TlsAlert) -> Bytes raise TlsAlert

The wire bytes for an alert (RFC 8446 §6): one record, two octets, never fragmented.

let
let tls_record_ccs : Bytes = b"\x14\x03\x03\x00\x01\x01"

The middlebox-compatibility change_cipher_spec record (RFC 8446 §5): always these six octets — the content type, the frozen version, a length of one, and the single octet 0x01 — always unencrypted, and never meaning anything.

item
extend TlsRecordEvent with Debug::
item
extend TlsRecordEvent with Eq::
enum
enum TlsServerState

A server's handshake state (RFC 8446 A.2).

fn
fn TlsServerState::new() -> TlsServerState

The initial state, awaiting the ClientHello.

fn
fn tls_server_recv( state : TlsServerState, msg_type : Int) -> TlsServerState raise TlsAlert

The next server state on receiving a handshake message of msg_type (RFC 8446 A.2): the ClientHello that opens the handshake, then the client's Certificate, CertificateVerify, and Finished. A message that does not belong in the current state raises the unexpected_message alert (§6.2), which is what the peer is owed.

fn
fn tls_server_send_flight( state : TlsServerState, request_client_cert : Bool) -> TlsServerState raise TlsAlert

The server's own action after receiving the ClientHello: it negotiates and sends its whole flight, then waits for the client's Certificate (when request_client_cert is set) or straight for the client Finished. Sending the flight in any other state is this endpoint's own bug, not the peer's, so it raises internal_error (§6.2).

item
extend TlsServerState with Debug::
item
extend TlsServerState with Eq::
struct
struct TranscriptHash

A running transcript hash over the handshake messages seen so far.

fn
fn TranscriptHash::new() -> TranscriptHash

A fresh, empty transcript.

fn
fn TranscriptHash::add(self : TranscriptHash, message : Bytes) -> Unit

Append a handshake message (its full HandshakeType + Length + body encoding) to the transcript.

fn
fn TranscriptHash::hash(self : TranscriptHash) -> Bytes

The SHA-256 transcript hash over every message added so far (RFC 8446 §4.4.1).

fn
fn tls13_finished_verify_data( base_key : Bytes, transcript_hash : Bytes) -> Bytes

The Finished verify_data (RFC 8446 §4.4.4): HMAC(finished_key, transcript_hash), where finished_key is HKDF-Expand-Label(base_key, "finished", "", Hash.length) over the sender's handshake traffic secret.

fn
fn tls13_finished_verify( base_key : Bytes, transcript_hash : Bytes, verify_data : Bytes) -> Bool

Whether a received Finished's verify_data is the one expected for base_key over transcript_hash — a constant set membership, the handshake authentication check.

struct
struct TlsExtension

A TLS extension: a two-byte type and its opaque data (RFC 8446 §4.2).

let
let tls_client_hello : Int = 1

The handshake message types this layer models (RFC 8446 §4).

let
let tls_server_hello : Int = 2

ServerHello (RFC 8446 §4.1.3): the server's half of the key exchange.

fn
fn tls_encode_handshake(msg_type : Int, body : Bytes) -> Bytes

Frame a handshake body: message type, then a 24-bit big-endian length (RFC 8446 §4).

fn
fn tls_parse_handshake(b : BytesView) -> (Int, Bytes)?

Parse one handshake message, returning its type and body, or None if truncated.

fn
fn tls_find_extension( exts : Array[TlsExtension], ext_type : Int) -> TlsExtension?

Look up the first extension of a given type, or None.

struct
struct TlsClientHello

A ClientHello (RFC 8446 §4.1.2), less the fixed legacy fields.

fn
fn encode_client_hello(ch : TlsClientHello) -> Bytes

Encode a ClientHello as a full handshake message. legacy_version is 0x0303 and legacy_compression_methods is the single null method, per RFC 8446 §4.1.2.

fn
fn decode_client_hello(body : BytesView) -> TlsClientHello?

Decode a ClientHello from a handshake body (RFC 8446 §4.1.2).

struct
struct TlsServerHello

A ServerHello (RFC 8446 §4.1.3): a single chosen cipher suite, no compression.

fn
fn encode_server_hello(sh : TlsServerHello) -> Bytes

Encode a ServerHello as a full handshake message.

fn
fn decode_server_hello(body : BytesView) -> TlsServerHello?

Decode a ServerHello from a handshake body (RFC 8446 §4.1.3).

item
extend TlsClientHello with Debug::
item
extend TlsClientHello with Eq::
item
extend TlsExtension with Debug::
item
extend TlsExtension with Eq::
item
extend TlsServerHello with Debug::
item
extend TlsServerHello with Eq::
struct
struct TlsCert

TLS certificate material for HTTPS serving (← uvicorn's ssl_certfile / ssl_keyfile). The backend the moonbitlang/async TLS layer uses is platform-specific, so both forms are carried: * certificate_file + private_key_file — PEM files, used by the OpenSSL backend (Linux / macOS, the CI platforms); * pfx_file — a PKCS#12 bundle, used by the SChannel backend (Windows). serve_tls picks the right pair at compile time. Supply whichever your deployment targets; a self-signed certs/ pair for tests is generated with scripts/gen_test_cert.sh.

item
async fn serve_tls( app : @moonasgi.AsgiApp, certificate_file~ : String, private_key_file~ : String, pfx_file~ : String, host? : String = "127.0.0.1", port? : Int = 8443, backlog? : Int = 2048) -> Unit

Serve a moonasgi ASGI application over native HTTP/1.1 **over TLS** (HTTPS, ← uvicorn's --ssl-certfile/--ssl-keyfile). Convenience wrapper over serve_tls_config that builds the Config from host/port/backlog and the certificate paths. Runs the full ASGI lifespan protocol around the accept loop (startup before the listener binds, shutdown on exit — even under cancellation), then, for every accepted connection, completes a TLS handshake and drives the app through the encrypted HTTP/1.1 codec. WebSocket-over-TLS (wss://) is not bridged here: the async websocket upgrade requires an @http.ServerConnection, which is welded to @socket.Tcp and cannot wrap a @tls.Tls stream. Plaintext serve retains full WebSocket support; this is a transport-capability boundary, not a behavioural choice (README §TLS).

item
async fn serve_tls_config( app : @moonasgi.AsgiApp, config : Config, cert : TlsCert) -> Unit

Serve a moonasgi ASGI application over HTTPS under an explicit Config and TlsCert. Mirrors serve_config: lifespan startup is driven before the listener binds and shutdown on the way out (guarded by protect_from_cancel so it still runs when the serving task is cancelled), and each accepted connection is handled by handle_https_conn.

fn
fn x509_alg_ecdsa_sha256() -> Bytes

The ecdsa-with-SHA256 AlgorithmIdentifier (RFC 5758): a SEQUENCE of just the OID, ECDSA taking no parameters.

fn
fn x509_alg_ec_public_key() -> Bytes

The id-ecPublicKey with the prime256v1 (P-256) curve AlgorithmIdentifier (RFC 5480).

fn
fn x509_common_name(cn : String) -> Bytes

A Name with a single commonName attribute: RDNSequence → RelativeDistinguishedName (SET) → AttributeTypeAndValue (SEQUENCE of the commonName OID 2.5.4.3 and the UTF8String value).

fn
fn x509_validity(not_before : String, not_after : String) -> Bytes

A Validity: notBefore and notAfter as UTCTime YYMMDDHHMMSSZ.

fn
fn x509_subject_public_key_info(pub_key : EcdsaPublicKey) -> Bytes

A SubjectPublicKeyInfo for a P-256 key: the ecPublicKey/prime256v1 algorithm and the uncompressed point as a BIT STRING.

fn
fn x509_tbs_certificate( serial : Bytes, common_name : String, not_before : String, not_after : String, pub_key : EcdsaPublicKey) -> Bytes

The TBSCertificate (RFC 5280 §4.1.2): version v3 [0] EXPLICIT INTEGER 2, the serial number, the signature algorithm, the issuer, the validity, the subject, and the SubjectPublicKeyInfo. Self-signed, so issuer and subject are the same commonName. Extensions are omitted (a valid, minimal profile).

fn
fn x509_self_signed( key : EcdsaPrivateKey, serial : Bytes, common_name : String, not_before : String, not_after : String) -> Bytes

A self-signed X.509 certificate: build the TBSCertificate for key's public key, sign its DER with ES256, DER-encode the r/s signature (RFC 5280 requires the ECDSA-Sig-Value SEQUENCE, not the raw concatenation), and wrap TBS + algorithm + signature in the outer Certificate SEQUENCE.

let
let x509_oid_cn : String = "2.5.4.3"

commonName (X.520 §6.2.2), the attribute a name is usually read from.

let
let x509_eku_server_auth : String = "1.3.6.1.5.5.7.3.1"

id-kp-serverAuth: the purpose a TLS server certificate needs (RFC 5280 §4.2.1.12).

let
let x509_eku_client_auth : String = "1.3.6.1.5.5.7.3.2"

id-kp-clientAuth.

struct
struct Attr

One AttributeTypeAndValue: the attribute's OID in dotted-decimal form, and its value as text.

struct
struct Name

A distinguished name as it is encoded — a sequence of relative distinguished names, each a set of attributes. Nearly every certificate puts one attribute in each RDN, but name comparison is defined over the nesting, so the nesting is what is kept.

fn
fn Name::get(self : Name, oid : String) -> String?

The value of the first attribute with this OID, in encoding order.

fn
fn Name::cn(self : Name) -> String?

The name's commonName, if it has one.

enum
enum GeneralName

A subjectAltName entry (RFC 5280 §4.2.1.6). The forms that name a TLS peer are decoded; the rest keep their field number and contents, so a caller can see what was there instead of being told the extension was empty.

struct
struct Validity

A validity window, in seconds since the Unix epoch.

struct
struct Spki

A SubjectPublicKeyInfo: the algorithm OID, the parameter OID when the algorithm takes one (for an EC key, the named curve), and the key bits. Absent and NULL parameters both read as None, which is what every algorithm reachable from here means by either.

struct
struct Exts

The extensions a peer's certificate is judged on (RFC 5280 §4.2). key_usage bits are numbered from the top of the first octet — 0 digitalSignature, 5 keyCertSign. eku is None when the extension is absent, which means any purpose, as against a listed set that does not include the one being asked for. unknown_critical names the critical extensions this parser does not understand: §4.2 makes such a certificate unusable, and a caller can only obey that rule if it is told rather than left to assume the skip was safe.

struct
struct Cert

A parsed certificate. tbs is the exact encoding the signature covers and raw the whole certificate, both as views into the input: a signature is checked over the bytes that arrived, never over a re-encoding of what they were understood to mean.

fn
fn x509_parse(der : BytesView) -> Cert raise DerError

Parse a DER certificate (RFC 5280 §4.1). Raises on anything that is not exactly one well-formed certificate: a truncated or over-long field, a payload glued to the end, a version that does not admit the fields present, or an outer signature algorithm that disagrees with the one the signature actually covers.

fn
fn Spki::p256(self : Spki) -> EcdsaPublicKey raise DerError

The P-256 public key a SubjectPublicKeyInfo carries. Refuses any algorithm but id-ecPublicKey on prime256v1, refuses the compressed and hybrid point forms, and refuses a point that is not on the curve.

fn
fn Cert::valid_at(self : Cert, now : Int64) -> Bool

Whether now, in seconds since the Unix epoch, falls inside the certificate's validity window. Both ends are inclusive: RFC 5280 §4.1.2.5 gives them as the first and last instant the certificate is valid, not as an open interval.

fn
fn Cert::matches(self : Cert, host : String) -> Bool

Whether host is one of the certificate's subjectAltName dNSName identities. iPAddress entries are not consulted — an address is not a hostname, and reading one as the other is how a certificate for a name comes to be trusted for an address. A certificate with no subjectAltName matches nothing: the commonName stopped being an identity with RFC 6125.

fn
fn Cert::signed_by( self : Cert, key : EcdsaPublicKey) -> Bool raise DerError

Check the certificate's signature under key — its issuer's public key, or its own if it is self-signed. Only ecdsa-with-SHA256 is checked; another algorithm raises Unsupported rather than returning false, so a caller cannot read "not checked" as "checked and bad".

item
extend Attr with Debug::
item
extend Attr with Eq::
item
extend Exts with Debug::
item
extend Exts with Eq::
item
extend GeneralName with Debug::
item
extend GeneralName with Eq::
item
extend Name with Debug::
item
extend Name with Eq::
item
extend Spki with Debug::
item
extend Spki with Eq::
item
extend Validity with Debug::
item
extend Validity with Eq::
fn
fn der_length(n : Int) -> Bytes

Encode a definite length (X.690 §8.1.3): the short form is the single byte for lengths under 128; the long form is 0x80 | n followed by the length in n big-endian bytes.

fn
fn der_tlv(tag : Int, value : Bytes) -> Bytes

A tag-length-value: the tag byte, the definite length of value, then value.

fn
fn der_integer(magnitude : Bytes) -> Bytes

A DER INTEGER (tag 0x02) from a big-endian magnitude: strip redundant leading zero bytes, then prepend one 0x00 if the top bit is set, so the value stays non-negative (X.690 §8.3).

fn
fn der_oid(arcs : Array[Int]) -> Bytes

A DER OBJECT IDENTIFIER (tag 0x06) from its arcs (X.690 §8.19): the first byte encodes 40·arc1 + arc2, and each later arc is base-128 big-endian with the high bit set on every byte but the last.

fn
fn der_bit_string(content : Bytes) -> Bytes

A DER BIT STRING (tag 0x03) with no unused trailing bits: a leading 0x00 count then content.

fn
fn der_octet_string(content : Bytes) -> Bytes

A DER OCTET STRING (tag 0x04).

fn
fn der_sequence(elements : Array[Bytes]) -> Bytes

A DER SEQUENCE (tag 0x30): the concatenated elements.

fn
fn der_set(elements : Array[Bytes]) -> Bytes

A DER SET (tag 0x31): the concatenated elements.

fn
fn der_null() -> Bytes

A DER NULL (tag 0x05, empty).

fn
fn der_utf8_string(s : String) -> Bytes

A DER UTF8String (tag 0x0c).

fn
fn der_utc_time(s : String) -> Bytes

A DER UTCTime (tag 0x17), the YYMMDDHHMMSSZ form X.509 validity uses before 2050.

fn
fn der_explicit(tag_num : Int, content : Bytes) -> Bytes

A context-specific constructed value [tag_num] wrapping content (tag 0xA0 | tag_num) — the EXPLICIT tagging X.509 uses for the certificate version and extensions.

item
suberror DerError

A refused encoding. Malformed is a hostile or corrupt certificate; Unsupported is well-formed DER this parser deliberately does not read, and the two are separated so a caller can log an interop gap without calling it an attack.

item
impl Show for DerError with fn output(self, logger)

DER errors print as the fault they are, so a rejected certificate names its own reason.

struct
struct Tlv

One decoded element: the tag byte, the contents, and raw — tag, length and contents together. raw is what a signature covers and how far to step to reach the next element.

struct
struct Bits

A decoded BIT STRING: the content bytes and how many low bits of the last byte are padding.

fn
fn Bits::get(self : Bits, i : Int) -> Bool

Whether bit i is set, counting from the most significant bit of the first byte — the numbering X.509 uses for keyUsage. Bits inside the padding, or past the end, read as unset.

fn
fn der_read(input : BytesView) -> Tlv raise DerError

Read the element at the head of input. Raises rather than reading past the end: the length is checked against the bytes that remain, in 64-bit arithmetic so a length near 2^31 cannot wrap the check.

fn
fn der_only(input : BytesView) -> Tlv raise DerError

Read the one element that is the whole of input. Bytes after it are an error — a certificate with a payload glued to its end is two documents, and the second one is not covered by the signature over the first.

fn
fn der_context(tag : Int, n : Int) -> Bool

Whether tag is the context-specific [n] of an IMPLICIT or EXPLICIT field.

fn
fn Tlv::items(self : Tlv) -> Array[Tlv] raise DerError

The elements a constructed value holds, in encoding order.

fn
fn Tlv::bool(self : Tlv) -> Bool raise DerError

A BOOLEAN. DER admits only 0x00 and 0xFF, so any other octet is refused.

fn
fn Tlv::int(self : Tlv) -> BigInt raise DerError

An INTEGER, two's complement as X.690 §8.3 defines it.

fn
fn Tlv::octets(self : Tlv) -> BytesView raise DerError

An OCTET STRING's contents.

fn
fn Tlv::bits(self : Tlv) -> Bits raise DerError

A BIT STRING.

fn
fn Tlv::oid(self : Tlv) -> String raise DerError

An OBJECT IDENTIFIER in dotted-decimal form. The first octet packs two arcs (X.690 §8.19.4); the rest are base-128, high bit set on every byte but an arc's last.

fn
fn Tlv::text(self : Tlv) -> String raise DerError

A DirectoryString or IA5String as text — the string types an X.509 Name and a SAN entry are written in. UniversalString is refused: it is UTF-32 and no certificate in practice uses it.

fn
fn Tlv::time(self : Tlv) -> Int64 raise DerError

A UTCTime or GeneralizedTime as seconds since the Unix epoch. RFC 5280 §4.1.2.5 pins both to the seconds-and-Z form, and a two-digit year to 1950..2049, so the other shapes X.690 allows are refused rather than guessed at.

item
extend Bits with Debug::
item
extend Bits with Eq::
item
extend DerError with Eq::
item
extend DerError with Show::

§QUIC

RFC 9000/9001 from the packet up: header protection, packet-number spaces, initial and handshake keys, frames, streams and their scheduler, flow control, loss recovery, congestion control, RTT estimation, retry, the connection state machines on both ends, and the server event loop that demultiplexes datagrams onto them over a UDP socket.

struct
struct AckRangeSet

The set of received packet numbers, held as ascending, non-overlapping, non-adjacent inclusive ranges [lo, hi].

fn
fn AckRangeSet::new() -> AckRangeSet

An empty set (nothing received yet).

fn
fn AckRangeSet::is_empty(self : AckRangeSet) -> Bool

Whether nothing has been received.

fn
fn AckRangeSet::largest(self : AckRangeSet) -> UInt64?

The largest packet number received, or None when empty.

fn
fn AckRangeSet::contains(self : AckRangeSet, pn : UInt64) -> Bool

Whether packet number pn has been received.

fn
fn AckRangeSet::add(self : AckRangeSet, pn : UInt64) -> Unit

Record that packet number pn was received, coalescing it with any range it touches or bridges.

fn
fn AckRangeSet::to_ack_fields( self : AckRangeSet) -> (UInt64, UInt64, Array[(UInt64, UInt64)])?

The ACK frame fields for the current set (RFC 9000 §19.3): the Largest Acknowledged, the First ACK Range (how many contiguous packets below the largest are acked), and the descending (Gap, ACK Range Length) list. None when nothing has been received.

fn
fn ack_fields_to_ranges( largest : UInt64, first_range : UInt64, pairs : Array[(UInt64, UInt64)]) -> Array[(UInt64, UInt64)] raise QuicAckError

Reconstruct the acknowledged packet-number ranges from an ACK frame's fields (the inverse of to_ack_fields), as ascending inclusive ranges — the receiver side that marks sent packets acknowledged.

item
suberror QuicAckError

A malformed ACK frame's ranges.

fn
fn quic_protect_short( spin : Bool, key_phase : Bool, dcid : Bytes, packet_number : Int64, pn_length : Int, payload : Bytes, key : Bytes, iv : Bytes, hp : Bytes) -> Bytes

Protect a QUIC 1-RTT packet: assemble the short header, AEAD-seal payload with the unprotected header as associated data, then apply header protection (RFC 9001 §5.3–§5.4).

fn
fn quic_unprotect_short( packet : Bytes, dcid_len : Int, key : Bytes, iv : Bytes, hp : Bytes) -> (Bytes, Int64)?

Remove protection from a received 1-RTT packet, given the length of the destination connection id this endpoint issued: the packet-number field starts right after the first byte and the connection id, and the payload runs to the end (RFC 9000 §17.3). The protected long-header trailer strip (header protection over the packet number, AEAD over the payload) is shared with the Initial/Handshake paths. None if it fails.

fn
fn quic_send_short( spin : Bool, key_phase : Bool, dcid : Bytes, packet_number : Int64, pn_length : Int, frames : Array[QuicFrame], keys : QuicPacketKeys) -> Bytes

Build a protected 1-RTT packet carrying frames: encode them into a payload and protect it with the application-space keys (RFC 9001 §5.3–§5.4).

fn
fn quic_recv_short( packet : Bytes, dcid_len : Int, keys : QuicPacketKeys) -> (Array[QuicFrame], Int64)? raise QuicPayloadError

Receive a protected 1-RTT packet with the application-space keys, given the length of the destination connection id this endpoint issued: remove protection, parse the payload, and return its frames and packet number. None if authentication fails.

struct
struct NewReno

A NewReno congestion controller for one connection (RFC 9002 §7).

fn
fn NewReno::new(max_datagram_size : Int64) -> NewReno

A fresh controller (RFC 9002 §7.2): the initial window is min(10·max_datagram_size, max(2·max_datagram_size, 14720)), the slow-start threshold is effectively unbounded, and nothing is in flight.

fn
fn NewReno::on_sent(self : NewReno, bytes : Int64) -> Unit

Record bytes of a packet sent into flight.

fn
fn NewReno::on_ack(self : NewReno, acked_bytes : Int64) -> Unit

Acknowledge acked_bytes of newly acknowledged data (RFC 9002 §7.3.1–§7.3.2): take it out of flight, then grow the window — by the acknowledged bytes in slow start, or by max_datagram_size · acked_bytes / cwnd in congestion avoidance.

fn
fn NewReno::on_packet_lost(self : NewReno, lost_bytes : Int64) -> Unit

Take lost_bytes of a declared-lost packet out of flight (RFC 9002 §7.3): the window reduction itself is a single on_congestion per recovery period.

fn
fn NewReno::on_congestion(self : NewReno) -> Unit

Enter a recovery period on a congestion signal (RFC 9002 §7.3.2): halve the window to the new slow-start threshold, floored at the minimum window of two datagrams.

fn
fn NewReno::can_send(self : NewReno, bytes : Int64) -> Bool

Whether bytes more may be sent without exceeding the congestion window.

fn
fn NewReno::window(self : NewReno) -> Int64

The congestion window (bytes).

fn
fn NewReno::ssthresh(self : NewReno) -> Int64

The slow-start threshold (bytes).

fn
fn NewReno::in_flight(self : NewReno) -> Int64

The bytes currently in flight.

enum
enum QuicLevel

A QUIC encryption level / packet-number space (RFC 9001 §4.1.1). 0-RTT shares the Application space, so the three long-lived spaces are Initial, Handshake, and Application.

struct
struct QuicConnection

A QUIC connection's per-space numbering/ACK state and per-level CRYPTO streams.

fn
fn QuicConnection::new(is_server : Bool) -> QuicConnection

A fresh connection (client or server) with empty spaces and CRYPTO streams.

fn
fn QuicConnection::is_server(self : QuicConnection) -> Bool

Whether this endpoint is the server.

fn
fn QuicConnection::space( self : QuicConnection, level : QuicLevel) -> PacketNumberSpace

The packet-number space for level.

fn
fn QuicConnection::crypto_stream( self : QuicConnection, level : QuicLevel) -> StreamReassembler

The CRYPTO reassembler for level.

fn
fn QuicConnection::next_packet_number( self : QuicConnection, level : QuicLevel) -> Int64

Allocate the next packet number to send at level.

fn
fn QuicConnection::on_packet_received( self : QuicConnection, level : QuicLevel, pn : Int64, ack_eliciting : Bool) -> Unit

Record that a packet numbered pn arrived at level; ack_eliciting marks one that must be acknowledged.

fn
fn QuicConnection::on_ack_received( self : QuicConnection, level : QuicLevel, largest : Int64) -> Unit

Record the peer's acknowledgement of up to largest at level.

fn
fn QuicConnection::build_ack( self : QuicConnection, level : QuicLevel, ack_delay : UInt64) -> QuicFrame?

Build the ACK frame owed at level, or None if nothing has been received there.

fn
fn QuicConnection::on_crypto_frame( self : QuicConnection, level : QuicLevel, offset : UInt64, data : Bytes) -> Bytes raise ReassemblyError

Feed a CRYPTO frame's data at offset and level into that level's handshake stream, returning the newly contiguous TLS handshake bytes now readable (empty while an earlier gap is still outstanding). CRYPTO streams carry no fin.

item
extend QuicLevel with Debug::
item
extend QuicLevel with Eq::
item
suberror FlowError

A flow-control violation: the peer sent past the limit we advertised (RFC 9000 §4.1), or our own accounting was asked to exceed the peer's limit.

struct
struct SendFlow

The send side of one flow-control window: bytes we have sent toward the peer's advertised limit.

fn
fn SendFlow::new(initial_max : UInt64) -> SendFlow

A send window with the peer's initial maximum.

fn
fn SendFlow::available(self : SendFlow) -> UInt64

How many more bytes may be sent right now (0 when blocked).

fn
fn SendFlow::is_blocked(self : SendFlow) -> Bool

Whether the window is exhausted (a STREAM_DATA_BLOCKED / DATA_BLOCKED condition).

fn
fn SendFlow::sent(self : SendFlow) -> UInt64

Total bytes sent so far.

fn
fn SendFlow::record_sent( self : SendFlow, n : UInt64) -> Unit raise FlowError

Account for sending n bytes; raises if that would exceed the peer's limit.

fn
fn SendFlow::update_limit(self : SendFlow, new_max : UInt64) -> Unit

Adopt a new limit from a MAX_DATA / MAX_STREAM_DATA frame; the limit only ever increases (an old, smaller value is ignored — RFC 9000 §4.1).

struct
struct RecvFlow

The receive side of one flow-control window: the peer may send up to the limit we advertised; we grow it as the application consumes data, keeping window bytes of headroom available.

fn
fn RecvFlow::new(window : UInt64) -> RecvFlow

A receive window of window bytes (the initial advertised limit).

fn
fn RecvFlow::limit(self : RecvFlow) -> UInt64

The current advertised limit.

fn
fn RecvFlow::record_received( self : RecvFlow, highest_offset : UInt64) -> Unit raise FlowError

Record that data up to absolute byte offset highest_offset has arrived; raises if it passes the limit we advertised (a flow-control violation).

fn
fn RecvFlow::consume(self : RecvFlow, n : UInt64) -> Unit

Account for the application consuming n more bytes, freeing window space.

fn
fn RecvFlow::should_extend(self : RecvFlow) -> Bool

Whether the limit should be extended: the limit we could advertise (consumed plus a full window) is at least half a window beyond the current one, so an update is worth sending (RFC 9000 §4.1 auto-tuning; avoids a MAX_DATA per byte).

fn
fn RecvFlow::extend_limit(self : RecvFlow) -> UInt64

Extend the advertised limit to consumed + window and return the new value — the MAX_DATA / MAX_STREAM_DATA to send.

enum
enum QuicFrame

A decoded QUIC frame. Padding collapses a run of zero bytes to its length; Stream carries application data on a stream, with a byte offset and a fin flag marking the end of the stream (RFC 9000 §19.8).

fn
fn encode_frame(f : QuicFrame) -> Bytes

Encode a frame to its wire bytes.

fn
fn parse_frame(b : BytesView) -> (QuicFrame, Int)?

Parse one frame at the start of b, returning it and the bytes it occupied, or None on a truncated frame or a type this brick does not yet decode. A PADDING frame absorbs the whole run of leading zero bytes.

item
extend QuicFrame with Debug::
item
extend QuicFrame with Eq::
fn
fn quic_protect_handshake( version : UInt, dcid : Bytes, scid : Bytes, packet_number : Int64, pn_length : Int, payload : Bytes, key : Bytes, iv : Bytes, hp : Bytes) -> Bytes

Protect a QUIC Handshake packet: assemble the long header, AEAD-seal payload with the unprotected header as associated data, then apply header protection (RFC 9001 §5.3–§5.4). sealed_len in the length field accounts for the 16-byte tag.

fn
fn quic_handshake_pn_offset(packet : Bytes) -> Int raise QuicPayloadError

The offset of the packet-number field in a received Handshake packet, walking its long header: first byte, version, the two connection ids, and the length field (RFC 9000 §17.2.4 — no token field, unlike Initial).

fn
fn quic_send_handshake( version : UInt, dcid : Bytes, scid : Bytes, packet_number : Int64, pn_length : Int, frames : Array[QuicFrame], keys : QuicPacketKeys) -> Bytes

Build a protected Handshake packet carrying frames: encode them into a payload and protect it with the handshake-space keys (RFC 9001 §5.3–§5.4).

fn
fn quic_recv_handshake( packet : Bytes, keys : QuicPacketKeys) -> (Array[QuicFrame], Int64)? raise QuicPayloadError

Receive a protected Handshake packet with the handshake-space keys: remove protection (the protected long-header trailer — header protection over the packet number, AEAD over the payload — is shared with the Initial path), parse the payload, and return its frames and packet number. None if authentication fails.

fn
fn quic_hp_mask(hp_key : Bytes, sample : Bytes) -> Bytes

The header-protection mask: the first five bytes of AES-128-ECB(hp_key, sample), where sample is 16 bytes of packet ciphertext (RFC 9001 §5.4.2/§5.4.3).

fn
fn quic_header_protect( packet : Bytes, pn_offset : Int, pn_length : Int, hp_key : Bytes) -> Bytes

Apply header protection in place-by-copy: XOR the first byte's form-appropriate low bits and the pn_length packet-number bytes at pn_offset with the mask. The 16-byte sample is taken at pn_offset + 4 (RFC 9001 §5.4.2), the fixed position that assumes the largest packet-number field.

fn
fn quic_header_unprotect( packet : Bytes, pn_offset : Int, hp_key : Bytes) -> (Bytes, Int)

Remove header protection: recover the first byte, read the packet-number length from its low two bits, unmask that many packet-number bytes, and return the unprotected packet together with the recovered length (RFC 9001 §5.4.1).

fn
fn tls13_client_hs_traffic_secret( handshake_secret : Bytes, transcript_hash : Bytes) -> Bytes

The client handshake traffic secret, Derive-Secret(Handshake Secret, "c hs traffic", ClientHello..ServerHello) (RFC 8446 §7.1).

fn
fn tls13_server_hs_traffic_secret( handshake_secret : Bytes, transcript_hash : Bytes) -> Bytes

The server handshake traffic secret, Derive-Secret(Handshake Secret, "s hs traffic", ClientHello..ServerHello).

fn
fn tls13_client_ap_traffic_secret( master_secret : Bytes, transcript_hash : Bytes) -> Bytes

The client application (1-RTT) traffic secret, Derive-Secret(Master Secret, "c ap traffic", ClientHello..server Finished).

fn
fn tls13_server_ap_traffic_secret( master_secret : Bytes, transcript_hash : Bytes) -> Bytes

The server application (1-RTT) traffic secret, Derive-Secret(Master Secret, "s ap traffic", ClientHello..server Finished).

fn
fn quic_handshake_keys( handshake_secret : Bytes, transcript_hash : Bytes, is_client : Bool) -> QuicPacketKeys

The QUIC Handshake-space AEAD key/iv/hp for this endpoint, derived from the handshake traffic secret over the ClientHello..ServerHello transcript (RFC 9001 §5.2).

fn
fn quic_application_keys( master_secret : Bytes, transcript_hash : Bytes, is_client : Bool) -> QuicPacketKeys

The QUIC Application (1-RTT) AEAD key/iv/hp for this endpoint, derived from the application traffic secret over the ClientHello..server Finished transcript.

struct
struct QuicPacketKeys

The AEAD packet-protection material for one direction: the 16-byte AES-128 key, the 12-byte IV, and the 16-byte header-protection key (RFC 9001 §5.1, §5.4).

fn
fn quic_initial_secret(dcid : Bytes) -> Bytes

The Initial secret shared by both endpoints: HKDF-Extract(initial_salt, DCID), where DCID is the Destination Connection ID from the client's first packet.

fn
fn quic_client_initial_secret(dcid : Bytes) -> Bytes

The client's Initial secret, HKDF-Expand-Label(initial, "client in", "", 32).

fn
fn quic_server_initial_secret(dcid : Bytes) -> Bytes

The server's Initial secret, HKDF-Expand-Label(initial, "server in", "", 32).

fn
fn quic_packet_keys(secret : Bytes) -> QuicPacketKeys

Derive the key/iv/hp triple from a direction's secret (RFC 9001 §5.1): the labels are "quic key", "quic iv", and "quic hp", at the AEAD_AES_128_GCM lengths.

item
extend QuicPacketKeys with Debug::
item
extend QuicPacketKeys with Eq::
fn
fn quic_initial_pn_offset(packet : Bytes) -> Int raise QuicPayloadError

The offset of the packet-number field in a received Initial packet, walking its long header: first byte, version, the two connection ids, the token, and the length field (RFC 9000 §17.2.2).

fn
fn quic_send_initial( version : UInt, dcid : Bytes, scid : Bytes, token : Bytes, packet_number : Int64, pn_length : Int, frames : Array[QuicFrame], keys : QuicPacketKeys) -> Bytes

Build a protected Initial packet carrying frames: encode them into a payload and protect it with keys (RFC 9001 §5.3, §5.4).

fn
fn quic_recv_initial( packet : Bytes, keys : QuicPacketKeys) -> (Array[QuicFrame], Int64)? raise QuicPayloadError

Receive a protected Initial packet with keys: remove protection, parse the payload, and return its frames and packet number. None if authentication fails.

fn
fn quic_protect_initial( version : UInt, dcid : Bytes, scid : Bytes, token : Bytes, packet_number : Int64, pn_length : Int, payload : Bytes, key : Bytes, iv : Bytes, hp : Bytes) -> Bytes

Protect a QUIC Initial packet: assemble the long header, AEAD-seal payload with the unprotected header as associated data (RFC 9001 §5.3), then apply header protection (§5.4). sealed_len in the length field accounts for the 16-byte tag.

fn
fn quic_unprotect_initial( packet : Bytes, pn_offset : Int, key : Bytes, iv : Bytes, hp : Bytes) -> (Bytes, Int64)?

Remove protection from a received Initial packet whose packet-number field starts at pn_offset: strip header protection, reconstruct the packet number and nonce, and AEAD-open the payload with the recovered header as associated data. Returns the plaintext payload and packet number, or None if authentication fails.

fn
fn quic_key_update(secret : Bytes) -> Bytes

The next-generation 1-RTT traffic secret, HKDF-Expand-Label(secret, "quic ku", "", Hash.length) (RFC 9001 §6.1). Applying it again advances another generation.

fn
fn quic_next_keys(secret : Bytes) -> (Bytes, QuicPacketKeys)

Roll to the next key generation: the updated traffic secret and the AEAD key/iv/hp derived from it.

struct
struct SentPacket

A sent, not-yet-acknowledged ack-eliciting packet (RFC 9002 §A.1): its number, the send time in microseconds, and the bytes it counts toward the congestion window.

struct
struct SentPacketTracker

The outstanding ack-eliciting packets in one packet-number space and the largest packet number acknowledged so far (-1 before any ACK).

fn
fn SentPacketTracker::new() -> SentPacketTracker

A fresh tracker with nothing outstanding.

fn
fn SentPacketTracker::on_sent( self : SentPacketTracker, pn : Int64, time_sent : Int64, size : Int64) -> Unit

Record that an ack-eliciting packet numbered pn was sent at time_sent (microseconds) carrying size bytes in flight.

fn
fn SentPacketTracker::on_ack( self : SentPacketTracker, acked : Array[Int64]) -> Int64

Process an ACK acknowledging the packet numbers acked: drop them from the outstanding set and advance largest_acked. Returns the bytes newly taken out of flight.

fn
fn quic_ack_frame_ranges(frame : QuicFrame) -> Array[(Int64, Int64)] raise

The acknowledged packet-number ranges an ACK frame conveys, as ascending inclusive (low, high) pairs (RFC 9000 §19.3), or an empty list for a frame that is neither ACK nor ACK_ECN — the ECN counts do not change which packets are acknowledged. Raises if the frame's ranges underflow the packet-number space.

fn
fn SentPacketTracker::on_ack_ranges( self : SentPacketTracker, ranges : Array[(Int64, Int64)]) -> Int64

Process an ACK whose acknowledged packets are the ascending inclusive ranges: drop every outstanding packet that falls in a range and advance largest_acked. Returns the bytes newly taken out of flight — the range-based path a real ACK frame drives.

fn
fn SentPacketTracker::detect_lost( self : SentPacketTracker, threshold : Int64) -> Array[Int64]

Declare lost every outstanding packet sent at least threshold packets before the largest acknowledged (RFC 9002 §6.1.1, kPacketThreshold = 3): a packet pn is lost when largest_acked - pn >= threshold. Returns the lost packet numbers and stops tracking them. Nothing is lost before the first ACK.

fn
fn SentPacketTracker::detect_lost_time( self : SentPacketTracker, now : Int64, time_threshold : Int64) -> Array[Int64]

Declare lost every outstanding packet sent before the largest acknowledged and older than time_threshold microseconds at now (RFC 9002 §6.1.2): lost when largest_acked > pn and now - time_sent >= time_threshold. Returns the lost packet numbers and stops tracking them.

fn
fn quic_loss_time_threshold( rtt : RttEstimator, granularity : Int64) -> Int64

The loss time threshold (RFC 9002 §6.1.2): max(kTimeThreshold · max(smoothed_rtt, latest_rtt), granularity) with kTimeThreshold = 9/8, in microseconds.

fn
fn SentPacketTracker::time_of( self : SentPacketTracker, pn : Int64) -> Int64?

The send time (microseconds) of the outstanding packet numbered pn, or None if it is not currently outstanding.

fn
fn SentPacketTracker::detect_lost_packets( self : SentPacketTracker, now : Int64, packet_threshold : Int64, time_threshold : Int64) -> Array[SentPacket]

Declare lost every outstanding packet meeting either the packet threshold (§6.1.1) or the time threshold (§6.1.2), returning the lost records — with their sizes, so the caller can take them out of the congestion window — and removing them from the outstanding set.

fn
fn SentPacketTracker::outstanding(self : SentPacketTracker) -> Array[Int64]

The packet numbers still outstanding (sent, not yet acknowledged or declared lost).

fn
fn SentPacketTracker::largest_acked(self : SentPacketTracker) -> Int64

The largest acknowledged packet number, or -1 before any ACK.

enum
enum QuicLongPacketType

The four long-header packet types (RFC 9000 §17.2), in the encoding of the first byte's type bits: Initial 0, 0-RTT 1, Handshake 2, Retry 3.

struct
struct QuicLongHeader

A long packet header's invariant fields: its type, the type-specific low four bits of the first byte (the packet-number length for Initial/Handshake/0-RTT, unused by Retry), the 32-bit version, and the destination and source connection IDs.

fn
fn encode_long_header(h : QuicLongHeader) -> Bytes

Encode a long header: first byte (1 header-form, 1 fixed bit, 2-bit type, 4 type-specific bits), the version big-endian, then each connection ID prefixed by its one-byte length.

fn
fn parse_long_header(b : BytesView) -> (QuicLongHeader, Int)?

Parse a long header at the start of b, returning it and the number of bytes it occupied, or None if the first byte is not a long header or b is truncated before the header ends.

item
extend QuicLongHeader with Debug::
item
extend QuicLongHeader with Eq::
item
extend QuicLongPacketType with Debug::
item
extend QuicLongPacketType with Eq::
fn
fn quic_pn_len(full_pn : Int64, largest_acked : Int64?) -> Int

The number of bytes (1–4) needed to encode full_pn given the largest packet number the peer has acknowledged (RFC 9000 §17.1 / A.2): enough bytes to cover twice the number of unacknowledged packets, so wraparound is unambiguous. With no acknowledgement yet, the count is full_pn + 1.

fn
fn quic_pn_encode(full_pn : Int64, largest_acked : Int64?) -> Bytes

Encode full_pn as its truncated big-endian packet number, using the shortest length that is unambiguous given largest_acked (RFC 9000 A.2).

fn
fn quic_pn_decode( largest_pn : Int64, truncated_pn : Int64, pn_nbits : Int) -> Int64

Recover the full packet number from a truncated_pn of pn_nbits bits, given the largest full packet number already received (RFC 9000 A.3): pick the value congruent to truncated_pn that is closest to the next expected number, resolving wraparound with the half-window rule.

item
suberror QuicPayloadError

A truncated or unparsable frame in a packet payload.

fn
fn quic_encode_payload(frames : Array[QuicFrame]) -> Bytes

Encode a list of frames into a packet payload — their wire encodings concatenated.

fn
fn quic_parse_payload( payload : Bytes) -> Array[QuicFrame] raise QuicPayloadError

Parse a decrypted packet payload into its frames, consuming the whole payload. A frame that does not parse, or that consumes no bytes, is a payload error.

fn
fn quic_frame_is_ack_eliciting(frame : QuicFrame) -> Bool

Whether a single frame is ack-eliciting (RFC 9000 §13.2.1): every frame except PADDING, ACK, ACK_ECN, and CONNECTION_CLOSE obliges the peer to acknowledge the packet.

fn
fn quic_payload_is_ack_eliciting(frames : Array[QuicFrame]) -> Bool

Whether a payload's frames make the packet ack-eliciting — true if any frame is.

fn
fn quic_pad_payload(payload : Bytes, min_size : Int) -> Bytes

Pad payload out to at least min_size bytes with PADDING frames (zero octets); a payload already that long is returned unchanged (RFC 9000 §14.1 — an Initial packet's payload is padded so the datagram reaches the 1200-byte minimum).

struct
struct PacketNumberSpace

One packet-number space's send/receive numbering and acknowledgement state.

fn
fn PacketNumberSpace::new() -> PacketNumberSpace

A fresh space: next packet number 0, nothing received or acknowledged.

fn
fn PacketNumberSpace::next_packet_number(self : PacketNumberSpace) -> Int64

Allocate the next packet number to send, advancing the counter.

fn
fn PacketNumberSpace::advance_past( self : PacketNumberSpace, pn : Int64) -> Unit

Skip the send counter past pn, so a packet another sender numbered in this space — the recovery loop's, which keeps its own counter — is not handed out again here. A pn this space is already past leaves it alone.

fn
fn PacketNumberSpace::largest_acked(self : PacketNumberSpace) -> Int64?

The largest packet number the peer has acknowledged, or None.

fn
fn PacketNumberSpace::on_packet_received( self : PacketNumberSpace, pn : Int64, ack_eliciting : Bool) -> Unit

Record that packet number pn arrived. ack_eliciting marks a packet that must be acknowledged (RFC 9000 §13.2.1); a pure-ACK packet is recorded but does not itself oblige a new ACK.

fn
fn PacketNumberSpace::on_ack_received( self : PacketNumberSpace, largest : Int64) -> Unit

Record that the peer acknowledged up to largest, advancing the high-water mark (an older ACK never lowers it).

fn
fn PacketNumberSpace::ack_pending(self : PacketNumberSpace) -> Bool

Whether an ACK is owed — an ack-eliciting packet has arrived since the last ACK was built.

fn
fn PacketNumberSpace::build_ack( self : PacketNumberSpace, ack_delay : UInt64) -> QuicFrame?

Build the ACK frame acknowledging everything received so far with the given ack_delay, and clear the pending flag. None when nothing has been received.

item
suberror ReassemblyError

A conflicting retransmission: a fragment overlaps already-buffered data with different bytes, or contradicts a known final size (RFC 9000 §2.2, §4.5).

struct
struct StreamReassembler

A stream reassembly buffer. consumed is the next byte offset the reader has not yet taken; intervals are the buffered runs beyond it, sorted and non-overlapping, each starting at or after consumed; final_size is the stream's total length once a fin has been seen.

fn
fn StreamReassembler::new() -> StreamReassembler

A fresh reassembler positioned at the start of the stream.

fn
fn StreamReassembler::consumed(self : StreamReassembler) -> UInt64

The next contiguous byte offset the reader has consumed.

fn
fn StreamReassembler::final_size(self : StreamReassembler) -> UInt64?

The stream's final size, if a fin has been received.

fn
fn StreamReassembler::is_complete(self : StreamReassembler) -> Bool

Whether the whole stream has arrived and been read (the read cursor reached the final size).

fn
fn StreamReassembler::insert( self : StreamReassembler, offset : UInt64, data : Bytes, fin : Bool) -> Unit raise ReassemblyError

Accept a fragment carrying data at offset, with fin marking it as the last. A fragment wholly before the read cursor is ignored; one that overlaps buffered bytes with a different value, or that contradicts a known final size, raises.

fn
fn StreamReassembler::read(self : StreamReassembler) -> Bytes

Take the contiguous run of bytes starting at the read cursor, advancing it past what is returned. Empty when the next expected offset has not arrived.

struct
struct QuicRecovery

A sender's recovery state for one packet-number space (RFC 9002).

fn
fn QuicRecovery::new( max_datagram_size : Int64, max_ack_delay : Int64, granularity : Int64) -> QuicRecovery

A fresh recovery state: max_datagram_size sizes the congestion window, max_ack_delay caps a peer's reported ACK delay, and granularity floors the timers — all microseconds except the datagram size in bytes. The packet-reordering threshold is kPacketThreshold (3).

fn
fn QuicRecovery::on_packet_sent( self : QuicRecovery, pn : Int64, now : Int64, size : Int64) -> Unit

Record an ack-eliciting packet numbered pn sent at now (microseconds) carrying size bytes: track it for acknowledgement and charge the congestion window.

fn
fn QuicRecovery::on_ack_received( self : QuicRecovery, frame : QuicFrame, now : Int64, ack_delay : Int64) -> Array[Int64] raise

Process a received ACK frame at now with the peer's reported ack_delay (RFC 9002 §5–§7): sample the RTT off the largest newly acknowledged packet, free the acknowledged bytes in the congestion window, then run loss detection over both thresholds. A non-empty loss is one congestion signal (halving the window) and frees the lost bytes. Returns the packet numbers declared lost — the frames to retransmit. Raises on a malformed ACK frame.

fn
fn QuicRecovery::can_send(self : QuicRecovery, bytes : Int64) -> Bool

Whether bytes more may be sent without exceeding the congestion window.

fn
fn QuicRecovery::window(self : QuicRecovery) -> Int64

The current congestion window (bytes).

fn
fn QuicRecovery::in_flight(self : QuicRecovery) -> Int64

The bytes currently in flight.

fn
fn QuicRecovery::smoothed_rtt(self : QuicRecovery) -> Int64

The smoothed RTT (microseconds).

fn
fn QuicRecovery::pto(self : QuicRecovery) -> Int64

The probe timeout (microseconds).

fn
fn QuicRecovery::pto_count(self : QuicRecovery) -> Int

The number of consecutive probe timeouts without an acknowledgement (the backoff exponent).

fn
fn QuicRecovery::pto_deadline(self : QuicRecovery) -> Int64?

The time (microseconds) at which the probe timeout fires, armed from the last ack-eliciting packet sent and backed off by 2^pto_count (RFC 9002 §6.2.1). None when nothing is outstanding — the timer is disarmed.

fn
fn QuicRecovery::on_pto(self : QuicRecovery) -> Unit

Handle a probe timeout (RFC 9002 §6.2.4): back off the timer for the next arming. The caller sends probe packets — retransmitting outstanding frames or new data.

fn
fn QuicRecovery::outstanding(self : QuicRecovery) -> Array[Int64]

The packet numbers still outstanding.

fn
fn quic_retry_integrity_tag( retry_without_tag : Bytes, odcid : Bytes) -> Bytes

The Retry integrity tag for a Retry packet (everything up to but not including the tag) given the original Destination Connection ID. The AEAD associated data is the Retry Pseudo-Packet: the ODCID length byte, the ODCID, then the Retry packet.

fn
fn quic_retry_verify(retry_packet : Bytes, odcid : Bytes) -> Bool

Verify a full Retry packet (ending in its 16-byte integrity tag): recompute the tag over the leading bytes and compare, accumulating the difference so the check does not exit early on the first mismatched byte.

struct
struct RttEstimator

An endpoint's RTT estimate for one packet-number space (RFC 9002 §5).

fn
fn RttEstimator::new() -> RttEstimator

A fresh estimator with no samples yet.

fn
fn RttEstimator::update( self : RttEstimator, latest_rtt : Int64, ack_delay : Int64, max_ack_delay : Int64) -> Unit

Fold in a new RTT sample (RFC 9002 §5.3). latest_rtt is the measured round trip and ack_delay the peer's reported delay before sending the ACK, both in microseconds; ack_delay is capped at max_ack_delay and subtracted only when doing so keeps the sample at or above min_rtt. The first sample seeds the estimate; later ones update the EWMA.

fn
fn RttEstimator::latest(self : RttEstimator) -> Int64

The latest RTT sample (microseconds).

fn
fn RttEstimator::min(self : RttEstimator) -> Int64

The minimum RTT seen (microseconds).

fn
fn RttEstimator::smoothed(self : RttEstimator) -> Int64

The smoothed RTT (microseconds).

fn
fn RttEstimator::variation(self : RttEstimator) -> Int64

The RTT variation, the mean deviation estimate (microseconds).

fn
fn RttEstimator::pto( self : RttEstimator, max_ack_delay : Int64, granularity : Int64) -> Int64

The Probe Timeout duration (RFC 9002 §6.2.1): smoothed_rtt + max(4·rttvar, granularity) + max_ack_delay. Before the first sample it is the initial RTT-based 2·initial_rtt; here, with no sample, it falls back to granularity + max_ack_delay.

struct
struct QuicSendFlow

The send-side flow control for a whole connection: the connection-wide window and a window per stream, each created at the peer's initial maximum on first use.

fn
fn QuicSendFlow::new( initial_max_data : UInt64, initial_max_stream_data : UInt64) -> QuicSendFlow

A fresh send-flow state at the peer's initial connection and per-stream maxima.

fn
fn QuicSendFlow::stream_window(self : QuicSendFlow, id : UInt64) -> UInt64

The bytes that may be sent on stream id right now: the smaller of the connection-wide and the stream's available windows (RFC 9000 §4.1).

fn
fn QuicSendFlow::record_stream_sent( self : QuicSendFlow, id : UInt64, n : UInt64) -> Unit raise FlowError

Account for sending n bytes on stream id, debiting both the stream and the connection window. Raises if either window would be exceeded — checked before debiting, so neither is left partially charged on failure (RFC 9000 §4.1).

fn
fn QuicSendFlow::on_max_data(self : QuicSendFlow, new_max : UInt64) -> Unit

Raise the connection-wide limit from a MAX_DATA frame.

fn
fn QuicSendFlow::on_max_stream_data( self : QuicSendFlow, id : UInt64, new_max : UInt64) -> Unit

Raise stream id's limit from a MAX_STREAM_DATA frame.

fn
fn QuicSendFlow::connection_available(self : QuicSendFlow) -> UInt64

The connection-wide bytes still sendable across all streams.

struct
struct QuicSender

A connection's send state for one packet-number space.

fn
fn QuicSender::new( initial_max_data : UInt64, initial_max_stream_data : UInt64, max_datagram_size : Int64, max_ack_delay : Int64, granularity : Int64, max_frame : UInt64) -> QuicSender

A fresh sender: initial_max_data/initial_max_stream_data are the peer's advertised flow limits, max_datagram_size sizes the congestion window, max_ack_delay/granularity tune the timers (microseconds), and max_frame caps a STREAM frame's payload.

fn
fn QuicSender::advance_past(self : QuicSender, pn : Int64) -> Unit

Skip the send counter past pn, so a packet this sender did not build — a driver's ACK-only packet in the same space — does not collide with a number it later assigns. A pn the sender is already past leaves it alone.

fn
fn QuicSender::queue_stream( self : QuicSender, id : UInt64, bytes : Bytes) -> Unit

Queue bytes of application data to send on stream id.

fn
fn QuicSender::queue_fin(self : QuicSender, id : UInt64) -> Unit

Mark stream id finished.

fn
fn QuicSender::on_max_data(self : QuicSender, new_max : UInt64) -> Unit

Raise the connection-wide send limit from a received MAX_DATA frame.

fn
fn QuicSender::poll_send( self : QuicSender, now : Int64) -> (Int64, Array[QuicFrame])? raise

The next packet to send at now, or None when the congestion window is closed or nothing is queued (RFC 9002 §7). Retransmissions go first, then freshly scheduled stream data; the packet is recorded in the recovery state and its stream sends remembered for retransmission.

fn
fn QuicSender::on_ack( self : QuicSender, frame : QuicFrame, now : Int64) -> Unit raise

Process a received ACK frame at now: clear acknowledged packets from the recovery state and re-queue the stream data of any packet the ACK reveals as lost, to be retransmitted by a later poll_send (RFC 9002 §6). Raises on a malformed ACK.

fn
fn QuicSender::on_pto_timeout(self : QuicSender, now : Int64) -> Bool

Handle the probe timeout firing at now (RFC 9002 §6.2.4). If the PTO deadline has passed with packets still outstanding, re-queue the oldest outstanding packet's stream data as a probe — a later poll_send puts it back on the wire — and back off the timer for the next arming. Unlike loss detection, this neither declares packets lost nor reduces the congestion window. Returns whether a probe was armed.

fn
fn QuicSender::outstanding(self : QuicSender) -> Array[Int64]

The packet numbers still outstanding (sent, not yet acknowledged or declared lost).

fn
fn QuicSender::window(self : QuicSender) -> Int64

The current congestion window (bytes).

enum
enum QuicConnPhase

Where a connection is in its lifecycle (RFC 9000 §10.2): exchanging packets, closing — this endpoint sent a CONNECTION_CLOSE and answers anything further with it — or draining, where the peer closed and nothing more goes out.

let
let quic_min_initial_datagram : Int = 1200

The smallest a datagram carrying an ack-eliciting Initial packet may be (RFC 9000 §14.1).

let
let quic_amplification_factor : Int64 = 3L

The anti-amplification factor (RFC 9000 §8.1): until it has validated a peer's address, a server may send it at most this many times the bytes it has received from it.

struct
struct QuicServeConfig

What a server gives each new connection: the QUIC version its long headers carry, the idle timeout it advertises, how long a closing or draining connection lingers, and the datagram size, timer tuning and flow-control credit the send loop starts from. Every duration is in microseconds, the unit the recovery code measures in.

fn
fn QuicServeConfig::default() -> QuicServeConfig

QUIC version 1, a 30-second idle timeout, a 3-second closing period, 1200-byte datagrams, a 25-millisecond ACK delay, and a mebibyte of flow-control credit on the connection and on each stream.

fn
fn quic_effective_idle_timeout(ours : Int64, theirs : Int64) -> Int64

The idle timeout in force on a connection (RFC 9000 §10.1): the smaller of the two endpoints' advertised max_idle_timeouts, where zero on either side means that end asks for no limit at all.

struct
struct QuicServeConn[A]

One server-side connection: the peer it answers, the connection ids the two ends address each other by, the handshake and send state machines it drives, and the counters the amplification limit, the idle timer and the closing period are read from. A is the peer-address type the enclosing QuicServer was built with.

item
fn[A] QuicServeConn::peer(self : QuicServeConn[A]) -> A

The peer this connection answers.

item
fn[A] QuicServeConn::cid(self : QuicServeConn[A]) -> Bytes

The connection id the peer addresses this server by — the table's key.

item
fn[A] QuicServeConn::phase(self : QuicServeConn[A]) -> QuicConnPhase

Where this connection is in its lifecycle.

item
fn[A] QuicServeConn::core(self : QuicServeConn[A]) -> QuicServerConn

The handshake state machine underneath, for a caller driving the TLS flights.

item
fn[A] QuicServeConn::rx_bytes(self : QuicServeConn[A]) -> Int64

Bytes received from the peer, the numerator of the amplification limit.

item
fn[A] QuicServeConn::tx_bytes(self : QuicServeConn[A]) -> Int64

Bytes sent to the peer, the quantity the amplification limit caps.

item
fn[A] QuicServeConn::packets_received(self : QuicServeConn[A]) -> Int

Datagrams received on this connection.

item
fn[A] QuicServeConn::validated(self : QuicServeConn[A]) -> Bool

Whether the peer's address has been validated (RFC 9000 §8.1), which lifts the amplification limit.

item
fn[A] QuicServeConn::idle_timeout(self : QuicServeConn[A]) -> Int64

The idle timeout in force, in microseconds; zero means none.

item
fn[A] QuicServeConn::pending(self : QuicServeConn[A]) -> Int

Datagrams built and waiting to go out — non-zero while the amplification limit or a closed congestion window is holding them back.

item
fn[A] QuicServeConn::largest_acked( self : QuicServeConn[A], level : QuicLevel) -> Int64?

The largest packet number the peer has acknowledged at level, or None before its first ACK there.

item
fn[A] QuicServeConn::can_send(self : QuicServeConn[A], n : Int) -> Bool

Whether n more bytes may go to this peer: unrestricted once its address is validated, otherwise at most three times what it has sent this server (RFC 9000 §8.1).

item
fn[A] QuicServeConn::install_handshake_keys( self : QuicServeConn[A], client_secret : Bytes, server_secret : Bytes) -> Unit

Install the Handshake-space keys, derived from the client's and this server's handshake traffic secrets (RFC 9001 §5.1). Until they are installed, a Handshake packet cannot be unprotected and is dropped; after, the connection reads and writes at that level. The client's secret is kept so an arriving Finished can be verified against it.

item
fn[A] QuicServeConn::install_app_keys( self : QuicServeConn[A], client_secret : Bytes, server_secret : Bytes) -> Unit

Install the Application-space (1-RTT) keys, derived from the two application traffic secrets. Until they are installed, a short-header packet is dropped and the send loop has nowhere to put stream data.

item
fn[A] QuicServeConn::apply_peer_params( self : QuicServeConn[A], params : Array[TransportParam]) -> Unit

Apply the peer's transport parameters (RFC 9000 §18). max_idle_timeout (§18.2) is the one that changes this connection: it settles the idle timer at the smaller of the two ends' advertised values, converting the parameter's milliseconds to the timers' microseconds.

item
fn[A] QuicServeConn::send_level(self : QuicServeConn[A]) -> QuicLevel

The highest encryption level this connection can send at — Application once its 1-RTT keys are in, else Handshake, else Initial, which is always available.

item
fn[A] QuicServeConn::send( self : QuicServeConn[A], level : QuicLevel, frames : Array[QuicFrame]) -> Unit

Queue frames to go out at level on the next poll_out, numbered from that level's packet-number space. An Initial packet carrying anything ack-eliciting takes its datagram to the 1200-byte floor (RFC 9000 §14.1); a level whose keys are not installed drops the frames, since there is nothing to protect them with.

item
fn[A] QuicServeConn::queue_stream( self : QuicServeConn[A], id : UInt64, bytes : Bytes) -> Unit

Queue bytes to send on stream id; the send loop frames and paces them out through poll_out once the Application keys are installed.

item
fn[A] QuicServeConn::take_received( self : QuicServeConn[A]) -> Array[(QuicLevel, QuicFrame)]

Take the frames received since the last call, each with the level it arrived at — where an application layer reads its streams out of.

item
fn[A] QuicServeConn::close( self : QuicServeConn[A], error_code : UInt64, reason : Bytes, now : Int64) -> Unit

Close the connection (RFC 9000 §10.2): put a CONNECTION_CLOSE carrying error_code and reason on the wire at the highest level with keys, drop whatever else was queued, and enter the closing period — during which every packet the peer sends is answered with the same CONNECTION_CLOSE, and at the end of which tick forgets the connection. Closing a connection that is already closing or draining does nothing.

struct
struct QuicServer[A]

A QUIC server: the connections it is holding, keyed by the connection id peers address each by, and the settings a new one starts from. A is the peer-address type — a socket address under the native event loop, whatever a test finds convenient under a fake sink.

item
fn[A] QuicServer::new(cfg : QuicServeConfig) -> QuicServer[A]

A server holding no connections, handing cfg to each one it opens.

item
fn[A] QuicServer::count(self : QuicServer[A]) -> Int

How many connections the server is holding.

item
fn[A] QuicServer::conn_ids(self : QuicServer[A]) -> Array[Bytes]

The connection ids the server is holding.

item
fn[A] QuicServer::conn( self : QuicServer[A], cid : Bytes) -> QuicServeConn[A]?

The connection cid addresses, or None if the server is not holding one.

item
fn[A] QuicServer::recv( self : QuicServer[A], datagram : Bytes, from : A, now : Int64) -> Unit raise

Take a datagram received at now from from: route it to the connection its Destination Connection ID names, opening one when an Initial packet arrives for an id the server does not hold, and drive that connection with it (RFC 9000 §5.2). A datagram naming no connection is dropped — RFC 9000 §10.3's stateless reset is not implemented.

item
fn[A] QuicServer::poll_out( self : QuicServer[A], now : Int64) -> (Bytes, A)? raise

The next datagram to put on the wire and the peer to send it to, or None when no connection has one ready.

item
fn[A] QuicServer::close( self : QuicServer[A], cid : Bytes, error_code : UInt64, reason : Bytes, now : Int64) -> Unit

Close the connection cid addresses with error_code and reason, if the server holds one (RFC 9000 §10.2).

item
fn[A] QuicServer::tick(self : QuicServer[A], now : Int64) -> Unit

Run the timers at now. An idle connection is forgotten without a CONNECTION_CLOSE, which is what RFC 9000 §10.1 asks for; a closing or draining one is forgotten when its period ends (§10.2); an active one whose probe timeout has come re-queues its oldest outstanding packet, to go out on the next poll_out (RFC 9002 §6.2.4).

item
extend QuicConnPhase with Debug::
item
extend QuicConnPhase with Eq::
fn
fn quic_now() -> Int64

The event loop's clock, in the microseconds the recovery and idle timers are measured in. @async.now() counts milliseconds since the epoch.

item
async fn quic_serve_endpoint( ep : QuicUdpEndpoint, server : QuicServer[@socket.Addr], tick_ms? : Int = 20) -> Unit

Serve QUIC on ep until the enclosing task group is cancelled: one task reads datagrams and demultiplexes them onto connections, the other runs the idle, probe and closing-period timers every tick_ms milliseconds, and both push out whatever that left ready to send. A datagram that names no connection or fails to authenticate is dropped rather than killing the loop — an endpoint cannot tell a stray packet from an attack.

item
async fn quic_serve( addr : String, server : QuicServer[@socket.Addr], tick_ms? : Int = 20) -> Unit

Bind a UDP socket at addr (e.g. "0.0.0.0:443") and serve server on it. The socket outlives the loop deliberately: closing one a task is parked in recvfrom on wedges it rather than waking it, so shutdown is cancellation of the task group, not a close.

struct
struct QuicServerConn

A QUIC server connection mid-handshake: the Initial keys, the packet-number/CRYPTO bookkeeping, and the TLS handshake it is driving.

fn
fn QuicServerConn::new(dcid : Bytes) -> QuicServerConn

A server connection for a client whose Destination Connection ID is dcid (the Initial secret and keys derive from it, RFC 9001 §5.2).

fn
fn QuicServerConn::handshake_state(self : QuicServerConn) -> TlsServerState

The current TLS handshake state.

fn
fn QuicServerConn::transcript_hash(self : QuicServerConn) -> Bytes

The transcript hash over the handshake messages processed so far.

fn
fn QuicServerConn::client_hello(self : QuicServerConn) -> Bytes

The raw ClientHello the connection received (empty before one arrives): the source a server runs the ECDHE key_share from to derive its handshake secrets.

fn
fn QuicServerConn::negotiate( self : QuicServerConn) -> TlsNegotiation raise TlsAlert

Negotiate the ClientHello this connection received (RFC 8446 §4.1.1): the group and key share to run the ECDHE with, and the signature scheme to sign the CertificateVerify under, read off what the client offered. Raises the §6 alert for a ClientHello this build cannot serve, and returns Retry when the answer is a HelloRetryRequest.

fn
fn QuicServerConn::initial_ack_pending(self : QuicServerConn) -> Bool

Whether an ACK is owed in the Initial space.

fn
fn QuicServerConn::send_server_hello( self : QuicServerConn, server_hello : TlsServerHello, out_dcid : Bytes, out_scid : Bytes, pn : Int64) -> Bytes

Build the server's ServerHello response: encode it, fold it into the transcript, and protect it into an Initial packet addressed to out_dcid (the client's source connection id) from out_scid, at Initial packet number pn (RFC 9001 §5.3). The state stays at RECVD_CH — the server flight is not complete until the Handshake-space messages (EncryptedExtensions, Certificate, CertificateVerify, Finished) are sent by send_handshake_flight, which advances it to WAIT_FINISHED.

fn
fn QuicServerConn::send_handshake_flight( self : QuicServerConn, server_hs_secret : Bytes, encrypted_extensions : Bytes, certificate : Bytes, certificate_verify : Bytes, out_dcid : Bytes, out_scid : Bytes, pn : Int64) -> Bytes raise

Send the server's Handshake-space flight (RFC 8446 §4, RFC 9001 §5): encode EncryptedExtensions, Certificate, and CertificateVerify, fold them into the transcript in order, compute the server Finished as HMAC(finished_key, transcript hash through CertificateVerify) over server_hs_secret, fold the Finished too, and advance the state past the whole flight (RECVD_CH → WAIT_FINISHED). The four messages form one contiguous CRYPTO stream, protected into a Handshake packet with the handshake-space keys quic_packet_keys(server_hs_secret), addressed to out_dcid from out_scid at Handshake packet number pn. server_hs_secret is the server handshake traffic secret the key schedule derives once ECDHE completes.

fn
fn QuicServerConn::send_handshake_flight_signed( self : QuicServerConn, server_hs_secret : Bytes, encrypted_extensions : Bytes, certificate : Bytes, signing_key : EcdsaPrivateKey, out_dcid : Bytes, out_scid : Bytes, pn : Int64) -> Bytes raise

Send the server's Handshake-space flight with a real CertificateVerify (RFC 8446 §4.4.3): like send_handshake_flight, but instead of taking the CertificateVerify body it signs the transcript through the Certificate with the certificate's ES256 key. Encode EncryptedExtensions and Certificate, fold them, sign the transcript hash then standing (the hash through the Certificate) into the CertificateVerify body, fold it, then the server Finished. A peer holding the certificate's public key can verify the signature and authenticate the server — the real handshake flight, no placeholder CertificateVerify.

fn
fn QuicServerConn::on_frames( self : QuicServerConn, level : QuicLevel, frames : Array[QuicFrame], pn : Int64) -> Array[Int] raise

Drive an already-unprotected packet's frames at level, numbered pn: record the packet for acknowledgement and feed its CRYPTO frames into that level's reassembler, driving the handshake with whatever handshake bytes become contiguous. Returns the handshake message types processed. A driver that unprotected the packet itself — because it also wants the ACK and CONNECTION_CLOSE frames beside the CRYPTO — enters here rather than through receive_initial.

fn
fn QuicServerConn::receive_initial( self : QuicServerConn, packet : Bytes) -> Array[Int] raise

Receive a client Initial packet: unprotect it, record its number for acknowledgement, reassemble its CRYPTO frames, and drive the handshake with the contiguous handshake bytes. Returns the handshake message types processed. Raises if the packet fails to authenticate.

fn
fn QuicServerConn::receive_handshake( self : QuicServerConn, packet : Bytes, client_hs_keys : QuicPacketKeys, client_hs_secret : Bytes) -> Bool raise

Receive the client's Handshake-space flight — in this no-client-authentication path, the client Finished (RFC 8446 §4.4.4): unprotect the Handshake packet with the client handshake keys, reassemble its CRYPTO stream, verify the Finished verify_data against client_hs_secret over the transcript through the server Finished, then fold it in to advance the handshake to CONNECTED. Returns whether the handshake is now connected with a Finished that authenticated. Raises if the packet fails to authenticate.

fn
fn QuicServerConn::on_handshake_frames( self : QuicServerConn, frames : Array[QuicFrame], pn : Int64, client_hs_secret : Bytes) -> Bool raise

Drive the client's Handshake-space frames, numbered pn, already unprotected: record the packet, reassemble its CRYPTO stream, verify any Finished in it against client_hs_secret over the transcript as it stands, then fold the messages in. Returns whether the handshake is now connected with a Finished that authenticated — the frame-level half of receive_handshake, for a driver holding the decrypted packet.

struct
struct QuicShortHeader

A short header's fields: the latency spin bit, the key-phase bit, the packet-number length (1–4 bytes, from the low two bits of the first byte plus one), and the destination connection ID.

fn
fn encode_short_header(h : QuicShortHeader) -> Bytes

Encode a short header: first byte (0 header-form, 1 fixed bit, spin, two reserved zero bits, key phase, and the 2-bit packet-number length minus one), then the destination connection ID with no length prefix. The (protected) packet number follows, encoded separately.

fn
fn parse_short_header( b : BytesView, dcid_len : Int) -> (QuicShortHeader, Int)?

Parse a short header at the start of b, given the length of the destination connection ID this endpoint issued. Returns the header and the bytes consumed (first byte plus the connection ID), or None if the first byte is a long header or b is shorter than that.

item
extend QuicShortHeader with Debug::
item
extend QuicShortHeader with Eq::
enum
enum StreamInitiator

Which endpoint opened a stream (RFC 9000 §2.1).

enum
enum StreamDirection

Whether a stream carries data one way or both (RFC 9000 §2.1).

fn
fn stream_initiator(id : UInt64) -> StreamInitiator

The endpoint that initiated stream id (bit 0).

fn
fn stream_direction(id : UInt64) -> StreamDirection

The directionality of stream id (bit 1).

fn
fn stream_is_bidi(id : UInt64) -> Bool

Whether stream id is bidirectional.

fn
fn stream_is_client_initiated(id : UInt64) -> Bool

Whether stream id was opened by the client.

fn
fn stream_sequence(id : UInt64) -> UInt64

The stream's sequence number within its type (the ID with its low two bits removed).

fn
fn stream_id_of( initiator : StreamInitiator, direction : StreamDirection, seq : UInt64) -> UInt64

The stream ID for the seq-th (0-based) stream of the given initiator and direction.

fn
fn stream_is_locally_initiated(id : UInt64, is_server : Bool) -> Bool

Whether stream id was opened by this endpoint, given whether it is the server.

fn
fn stream_is_writable(id : UInt64, is_server : Bool) -> Bool

Whether this endpoint may send on stream id: it may send on a stream it opened, or on a bidirectional stream its peer opened; it may not send on a peer-opened unidirectional (receive-only) stream (RFC 9000 §2.1, §3).

item
extend StreamDirection with Debug::
item
extend StreamDirection with Eq::
item
extend StreamInitiator with Debug::
item
extend StreamInitiator with Eq::
struct
struct StreamManager

The set of receive streams plus the connection-level receive window.

fn
fn StreamManager::new( conn_window : UInt64, stream_window : UInt64) -> StreamManager

A manager advertising conn_window of connection-level credit and stream_window per stream.

fn
fn StreamManager::on_stream_frame( self : StreamManager, id : UInt64, offset : UInt64, data : Bytes, fin : Bool) -> Bytes raise

Route a STREAM frame for id carrying data at offset (with fin on the last fragment): enforce the stream and connection flow-control limits, reassemble, and return the contiguous bytes now deliverable, charging that read against both windows.

fn
fn StreamManager::read(self : StreamManager, id : UInt64) -> Bytes

Deliver any further contiguous bytes now readable on id (empty if the stream is unknown or nothing new is contiguous), charging them against both windows.

fn
fn StreamManager::stream_count(self : StreamManager) -> Int

The number of streams currently tracked.

fn
fn StreamManager::connection_max_data_update( self : StreamManager) -> UInt64?

Whether the connection-level receive limit should be extended, and the new MAX_DATA value to advertise (RFC 9000 §4.1). Call extend counterpart on the returned flow if non-None.

struct
struct StreamOut

A stream's queued output: bytes still to frame, the next offset to send from, and whether a FIN is queued and has been emitted.

struct
struct StreamSend

A scheduling decision: send length bytes on stream at offset, ending the stream when fin.

struct
struct QuicStreamScheduler

A round-robin send scheduler over a connection's streams, bounded by flow and a maximum per-frame payload.

fn
fn QuicStreamScheduler::new( flow : QuicSendFlow, max_frame : UInt64) -> QuicStreamScheduler

A fresh scheduler over flow with frames capped at max_frame bytes.

fn
fn QuicStreamScheduler::queue( self : QuicStreamScheduler, id : UInt64, n : UInt64) -> Unit

Queue n more bytes of application data to send on stream id.

fn
fn QuicStreamScheduler::queue_fin( self : QuicStreamScheduler, id : UInt64) -> Unit

Mark stream id finished: a FIN will ride the frame that drains it, or a FIN-only frame.

fn
fn QuicStreamScheduler::on_max_data( self : QuicStreamScheduler, new_max : UInt64) -> Unit

Raise the connection-wide send limit from a MAX_DATA frame.

fn
fn QuicStreamScheduler::on_max_stream_data( self : QuicStreamScheduler, id : UInt64, new_max : UInt64) -> Unit

Raise stream id's send limit from a MAX_STREAM_DATA frame.

fn
fn QuicStreamScheduler::next( self : QuicStreamScheduler) -> StreamSend? raise FlowError

The next scheduling decision, or None when no stream can send (all drained or flow-control blocked). Round-robins fairly across the streams and debits the flow control for the bytes scheduled. Raises only on a flow-control accounting error, which the bounds here preclude.

enum
enum SendStreamState

The send half's state (RFC 9000 §3.1).

enum
enum SendStreamEvent

A send-half event: writing a STREAM frame (with fin on the last), all sent data being acknowledged, sending a RESET_STREAM, or that reset being acknowledged.

item
suberror StreamStateError

An illegal stream-state transition.

fn
fn send_stream_next( state : SendStreamState, event : SendStreamEvent) -> SendStreamState raise StreamStateError

The next send state for event in state (RFC 9000 §3.1). A stream may be reset from any pre-terminal state; writing after a terminal or reset state is illegal.

enum
enum RecvStreamState

The receive half's state (RFC 9000 §3.2).

enum
enum RecvStreamEvent

A receive-half event: a STREAM frame arriving (with fin fixing the final size), all data up to the final size received, the application reading all data, a RESET_STREAM arriving, or the application being notified of that reset.

fn
fn recv_stream_next( state : RecvStreamState, event : RecvStreamEvent) -> RecvStreamState raise StreamStateError

The next receive state for event in state (RFC 9000 §3.2). A stream may be reset from any pre-terminal state; the read events apply only once the data or reset has fully arrived.

item
extend RecvStreamEvent with Debug::
item
extend RecvStreamEvent with Eq::
item
extend RecvStreamState with Debug::
item
extend RecvStreamState with Eq::
item
extend SendStreamEvent with Debug::
item
extend SendStreamEvent with Eq::
item
extend SendStreamState with Debug::
item
extend SendStreamState with Eq::
let
let tp_max_idle_timeout : UInt64 = 0x01

The transport-parameter identifiers this build recognizes by name (RFC 9000 §18.2). The block is still encoded and decoded generically, so unknown ids pass through.

let
let tp_max_udp_payload_size : UInt64 = 0x03

The largest UDP payload this endpoint is willing to receive.

let
let tp_initial_max_data : UInt64 = 0x04

Connection-level flow-control credit the peer starts with.

let
let tp_initial_max_stream_data_bidi_local : UInt64 = 0x05

Starting credit on bidirectional streams this endpoint opens.

let
let tp_initial_max_stream_data_bidi_remote : UInt64 = 0x06

Starting credit on bidirectional streams the peer opens.

let
let tp_initial_max_stream_data_uni : UInt64 = 0x07

Starting credit on unidirectional streams.

let
let tp_initial_max_streams_bidi : UInt64 = 0x08

How many bidirectional streams the peer may open.

let
let tp_initial_max_streams_uni : UInt64 = 0x09

How many unidirectional streams the peer may open.

let
let tp_ack_delay_exponent : UInt64 = 0x0a

The exponent the peer's ACK Delay field is scaled by.

let
let tp_max_ack_delay : UInt64 = 0x0b

The longest this endpoint will sit on an acknowledgement.

let
let tp_active_connection_id_limit : UInt64 = 0x0e

How many connection ids this endpoint will keep active at once.

let
let tp_initial_source_connection_id : UInt64 = 0x0f

The source connection id this endpoint used on its first packet, which is what binds the handshake to the addresses it ran over.

struct
struct TransportParam

A transport parameter: an identifier and its raw value bytes. Integer parameters carry a varint-encoded value; use transport_param_int / transport_param_as_int to build and read those.

fn
fn transport_param_int(id : UInt64, value : UInt64) -> TransportParam

A transport parameter whose value is a single varint integer (RFC 9000 §18.2).

fn
fn transport_param_as_int(p : TransportParam) -> UInt64?

Read a varint-valued transport parameter's integer, or None if its value is not a single well-formed varint.

fn
fn encode_transport_params(params : Array[TransportParam]) -> Bytes

Encode a transport-parameter block: each parameter as its id varint, a length varint, then the value bytes (RFC 9000 §18).

fn
fn decode_transport_params(b : BytesView) -> Array[TransportParam]?

Decode a transport-parameter block, preserving order and duplicates so the caller can apply RFC 9000 §7.4's rules. Returns None on a truncated parameter.

item
extend TransportParam with Debug::
item
extend TransportParam with Eq::
struct
struct QuicUdpEndpoint

A QUIC UDP endpoint: a bound datagram socket.

item
async fn QuicUdpEndpoint::bind(addr : String) -> QuicUdpEndpoint

Bind an endpoint to addr (e.g. "0.0.0.0:443", or "127.0.0.1:0" for an OS-assigned port). The bound address is available from local_addr.

fn
fn QuicUdpEndpoint::local_addr(self : QuicUdpEndpoint) -> @socket.Addr

The address the endpoint is bound to (the concrete port when bound to port 0).

item
async fn QuicUdpEndpoint::recv_datagram( self : QuicUdpEndpoint, max? : Int = 2048) -> (Bytes, @socket.Addr)

Receive the next datagram and its sender's address, copying up to max bytes.

item
async fn QuicUdpEndpoint::send_datagram( self : QuicUdpEndpoint, data : Bytes, to : @socket.Addr) -> Unit

Send data as a single datagram to to.

fn
fn QuicUdpEndpoint::close(self : QuicUdpEndpoint) -> Unit

Close the endpoint's socket.

fn
fn quic_varint_encode(v : UInt64) -> Bytes

Encode v as a QUIC variable-length integer (RFC 9000 §16) in the shortest form that holds it: the two most-significant bits of the first byte select the length (00 → 1 byte / 6-bit value, 01 → 2 / 14-bit, 10 → 4 / 30-bit, 11 → 8 / 62-bit) and the remaining bits carry the value big-endian. v must be below 2^62 (QUIC's varint ceiling).

fn
fn quic_varint_decode(b : BytesView) -> (UInt64, Int)?

Decode a QUIC variable-length integer at the start of b, returning its value and the number of bytes it occupied, or None when b is shorter than the length its first byte's prefix declares.

§HTTP/3 and QPACK

RFC 9114 framing, settings and the request/response message model over QUIC streams, with the RFC 9204 QPACK codec: static and dynamic tables, field encoding, encoder/decoder instructions and section prefixes.

enum
enum Http3Role

Which side of the connection an endpoint is (RFC 9114 §3.1): a server must not receive a push stream, so the role decides whether one is a protocol violation.

item
suberror Http3ConnError

A connection-level HTTP/3 error (RFC 9114 §8): a broken control stream or a stream a peer must not have opened.

enum
enum Http3UniEvent

The result of accepting a peer's unidirectional stream: the control stream carries the peer's SETTINGS, the QPACK streams carry table instructions, a push stream carries its id, and an unrecognized type is reserved/greased.

struct
struct Http3Conn

One endpoint of an HTTP/3 connection: its role, the settings it advertises, and — once the peer opens its control stream — the peer's settings.

fn
fn Http3Conn::new( role : Http3Role, local_settings : Http3Settings) -> Http3Conn

A connection that will advertise local_settings.

fn
fn Http3Conn::open_control_stream(self : Http3Conn) -> Bytes

The bytes to write when opening our control stream: the control-stream type then our SETTINGS frame, which a control stream must send first (RFC 9114 §6.2.1).

fn
fn Http3Conn::accept_uni_stream( self : Http3Conn, bytes : Bytes) -> Http3UniEvent raise

Accept a peer's unidirectional stream from its opening bytes: classify it and, for the control stream, decode and keep the peer's SETTINGS. A second control stream, a control stream not opening with SETTINGS, and a server receiving a push stream are all connection errors (RFC 9114 §6.2).

fn
fn Http3Conn::settings_established(self : Http3Conn) -> Bool

Whether the peer's control-stream SETTINGS have been received (connection setup complete).

fn
fn Http3Conn::peer_settings(self : Http3Conn) -> Http3Settings?

The peer's advertised settings, once its control stream has been accepted.

item
extend Http3Role with Debug::
item
extend Http3Role with Eq::
item
extend Http3UniEvent with Debug::
item
extend Http3UniEvent with Eq::
item
suberror Http3FrameError

A malformed HTTP/3 frame (a protocol error, distinct from an incomplete read).

enum
enum Http3Frame

A decoded HTTP/3 frame. Data/Headers/PushPromise carry their opaque payloads; Settings carries the decoded (identifier, value) pairs. Reserved is any unknown or grease frame type (RFC 9114 §7.2.8 / §9), which a receiver ignores; ReservedHttp2 is one of the frame types reused from HTTP/2 (0x02, 0x06, 0x08, 0x09), which RFC 9114 §7.2 requires be treated as a connection error.

fn
fn http3_frame_encode(frame : Http3Frame) -> Bytes

Encode one HTTP/3 frame: its type, its payload length, then the payload.

fn
fn http3_settings_encode(pairs : Array[(UInt64, UInt64)]) -> Bytes

Encode a SETTINGS payload: each pair as its identifier varint then its value varint (RFC 9114 §7.2.4).

fn
fn http3_frame_decode( input : BytesView) -> (Http3Frame, Int)? raise Http3FrameError

Decode one HTTP/3 frame from the front of input. Returns Some((frame, consumed)) on a complete frame, None when more bytes are needed (a partial frame — the type, the length, or the payload has not fully arrived), and raises on a malformed frame (a truncated SETTINGS pair or a required varint field that is absent).

fn
fn http3_settings_decode( payload : Bytes) -> Array[(UInt64, UInt64)] raise Http3FrameError

Decode a SETTINGS payload into its (identifier, value) pairs. A trailing partial pair (an identifier with no value, or a truncated varint) is a frame error.

fn
fn http3_frame_decode_all( input : Bytes) -> (Array[Http3Frame], Int) raise Http3FrameError

Decode every complete frame at the front of input, returning the frames and the number of octets consumed (the tail, a partial frame, is left for the next read). A stream reader feeds accumulated bytes and keeps the unconsumed remainder.

item
extend Http3Frame with Debug::
item
extend Http3Frame with Eq::
enum
enum Http3StreamCtx

The stream a frame was received on, for the purpose of frame-placement rules (RFC 9114 §6.2).

fn
fn http3_frame_allowed(frame : Http3Frame, ctx : Http3StreamCtx) -> Bool

Whether frame is permitted on a stream of kind ctx (RFC 9114 §7.2).

fn
fn http3_check_frame( frame : Http3Frame, ctx : Http3StreamCtx) -> Unit raise Http3FrameError

Raise Http3FrameError (H3_FRAME_UNEXPECTED) when frame is not permitted on ctx.

item
extend Http3StreamCtx with Debug::
item
extend Http3StreamCtx with Eq::
fn
fn http3_encode_message( headers : Array[(String, String)], body : Bytes) -> Bytes

Encode a message body as an HTTP/3 request/response stream: a HEADERS frame with the QPACK-encoded headers (static table only), then a DATA frame when body is non-empty.

fn
fn http3_decode_message( bytes : Bytes) -> (Array[(String, String)], Bytes) raise Http3FrameError

Decode an HTTP/3 request/response stream back into its header list and body: the first HEADERS frame gives the headers, the DATA frames concatenate into the body, and a second HEADERS frame (trailers) and unknown frames are ignored (RFC 9114 §4.1). Raises if there is no HEADERS frame or a frame is malformed.

fn
fn http3_request( meth : String, scheme : String, authority : String, path : String, headers : Array[(String, String)], body : Bytes) -> Bytes

Encode an HTTP/3 request: the :method, :scheme, :authority, and :path pseudo- headers (RFC 9114 §4.3.1), then headers, then the body.

fn
fn http3_response( status : String, headers : Array[(String, String)], body : Bytes) -> Bytes

Encode an HTTP/3 response: the :status pseudo-header (RFC 9114 §4.3.2), then headers, then the body.

struct
struct Http3Request

A decoded HTTP/3 request: the pseudo-headers pulled out, the remaining header fields, and the body.

fn
fn http3_parse_request( bytes : Bytes) -> Http3Request? raise Http3FrameError

Decode an HTTP/3 request stream into a request, separating the pseudo-headers from the regular ones (RFC 9114 §4.3.1). None when the required :method and :path are absent. Raises on a malformed stream.

fn
fn http3_parse_request_strict( bytes : Bytes) -> Http3Request? raise Http3FrameError

Parse a request stream, rejecting any frame the protocol does not allow on a request stream (RFC 9114 §7.2): a control-only frame such as SETTINGS is an H3_FRAME_UNEXPECTED connection error here, not silently ignored. Otherwise as http3_parse_request — None when the required :method / :path are absent. This is the request path with frame-placement validation applied.

struct
struct Http3Response

An HTTP/3 response: the status, the header fields, and the body.

fn
fn Http3Response::encode(self : Http3Response) -> Bytes

Encode the response as an HTTP/3 response stream (a HEADERS frame with :status, then DATA).

fn
fn http3_serve( request_bytes : Bytes, handler : (Http3Request) -> Http3Response) -> Bytes raise Http3FrameError

Serve one HTTP/3 request stream: decode the request, apply handler, and encode its response stream. A request missing its pseudo-headers is answered with a 400. Raises on a malformed request stream.

let
let h3_settings_qpack_max_table_capacity : UInt64 = 0x01

SETTINGS_QPACK_MAX_TABLE_CAPACITY (RFC 9204 §5).

let
let h3_settings_max_field_section_size : UInt64 = 0x06

SETTINGS_MAX_FIELD_SECTION_SIZE (RFC 9114 §7.2.4.1).

let
let h3_settings_qpack_blocked_streams : UInt64 = 0x07

SETTINGS_QPACK_BLOCKED_STREAMS (RFC 9204 §5).

struct
struct Http3Settings

An endpoint's HTTP/3 settings.

fn
fn Http3Settings::new( qpack_max_table_capacity : UInt64, max_field_section_size : UInt64, qpack_blocked_streams : UInt64) -> Http3Settings

Settings with the given QPACK table capacity, maximum field section size, and blocked-stream limit.

fn
fn Http3Settings::to_pairs(self : Http3Settings) -> Array[(UInt64, UInt64)]

The (identifier, value) pairs to carry in a SETTINGS frame.

fn
fn Http3Settings::from_pairs( pairs : Array[(UInt64, UInt64)]) -> Http3Settings

Read settings from received (identifier, value) pairs, taking the value for each known identifier (0 when absent) and ignoring unknown ones (RFC 9114 §7.2.4.1 grease tolerance).

fn
fn http3_decode_control_stream( input : BytesView) -> Http3Settings? raise Http3FrameError

Decode an endpoint's control stream (RFC 9114 §6.2.1): its type prefix must be the control stream, and its first frame a SETTINGS frame, whose settings are returned. None if the stream is not a control stream or does not open with SETTINGS. Raises on a malformed frame.

item
extend Http3Settings with Debug::
item
extend Http3Settings with Eq::
enum
enum Http3UniStream

An HTTP/3 unidirectional stream's type (RFC 9114 §6.2, RFC 9204 §4.2).

fn
fn http3_encode_stream_type(stream : Http3UniStream) -> Bytes

Encode a unidirectional stream's opening bytes: the type varint, plus the push id for a push stream (RFC 9114 §6.2.2).

fn
fn http3_decode_stream_type(input : BytesView) -> (Http3UniStream, Int)?

Decode a unidirectional stream's opening bytes: the type varint (and a push id for a push stream). Returns the stream kind and bytes consumed, or None on a partial read.

fn
fn http3_encode_control_stream(settings : Array[(UInt64, UInt64)]) -> Bytes

Encode a control stream (RFC 9114 §6.2.1): the control-stream type, then a SETTINGS frame with settings, which a control stream must send first.

item
extend Http3UniStream with Debug::
item
extend Http3UniStream with Eq::
struct
struct QpackDynamicTable

A QPACK dynamic table: the live entries, the bytes they occupy, the capacity, the count of entries ever inserted (the next absolute index), and how many have been evicted.

fn
fn QpackDynamicTable::new(capacity : Int) -> QpackDynamicTable

A fresh, empty dynamic table with the given capacity in bytes.

fn
fn qpack_entry_size(name : Bytes, value : Bytes) -> Int

The size an entry occupies in the table: its name and value octets plus 32 (RFC 9204 §3.2.1).

fn
fn QpackDynamicTable::set_capacity( self : QpackDynamicTable, capacity : Int) -> Unit

Set the table capacity, evicting whatever no longer fits (RFC 9204 §3.2.3).

fn
fn QpackDynamicTable::insert( self : QpackDynamicTable, name : Bytes, value : Bytes) -> Bool

Insert an entry, evicting as needed. Returns false (adding nothing) when the entry cannot fit even in an empty table (RFC 9204 §3.2.2).

fn
fn QpackDynamicTable::get( self : QpackDynamicTable, abs_index : Int) -> (Bytes, Bytes)?

The entry at absolute index abs_index, or None if it has been evicted or never existed (RFC 9204 §3.2.4).

fn
fn QpackDynamicTable::get_relative( self : QpackDynamicTable, rel : Int) -> (Bytes, Bytes)?

The entry at encoder-stream relative index rel (0 = most recently inserted, RFC 9204 §3.2.5).

fn
fn QpackDynamicTable::apply( self : QpackDynamicTable, inst : QpackEncoderInst) -> Bool raise QpackError

Apply an encoder-stream instruction to the table (RFC 9204 §4.3): set the capacity, or insert an entry whose name comes from the static table, a dynamic entry (by relative index), or a literal. Returns whether the resulting insert fit (always true for a capacity change). Raises if a referenced name index is out of range.

fn
fn qpack_encode_dynamic( headers : Array[(String, String)], table : QpackDynamicTable, max_entries : Int, huffman? : Bool = true) -> (Bytes, Bytes)

Encode headers with dynamic indexing over table (the encoder's view, mutated by the inserts). Returns the encoder-stream instruction block and the field section. max_entries comes from the negotiated maximum table capacity.

item
suberror QpackError

A malformed or unsupported QPACK field section.

fn
fn qpack_encode_string( data : Bytes, prefix_bits : Int, pattern : Int, huffman : Bool) -> Bytes

Encode a QPACK string literal: the H bit at position prefix_bits (set when the data is Huffman-coded), pattern in the bits above it, the octet length as a prefix_bits-prefix integer, then the (raw or Huffman) octets.

fn
fn qpack_decode_string( input : BytesView, prefix_bits : Int) -> (Bytes, Int)? raise QpackError

Decode a QPACK string literal whose length uses a prefix_bits-prefix integer and whose H bit sits at position prefix_bits. Returns (octets, bytes-consumed), or None when the string has not fully arrived.

fn
fn qpack_encode_field_section( headers : Array[(String, String)], huffman? : Bool = true) -> Bytes

Encode a header list as a QPACK field section over the static table only: a zero-insert-count / zero-base prefix, then one field line per header — an indexed static entry for an exact match, a static name reference plus a literal value when the name is known, or a fully literal name and value otherwise. huffman chooses whether literal strings are Huffman-coded.

fn
fn qpack_decode_field_section( input : Bytes) -> Array[(String, String)] raise QpackError

Decode a QPACK field section (encoded over the static table) back into its header list. The prefix's insert count and base are read and required to be zero (this decoder resolves static references only); a field line that references the dynamic table is reported as unsupported.

fn
fn qpack_decode_field_section_dyn( input : Bytes, table : QpackDynamicTable, max_entries : Int) -> Array[(String, String)] raise QpackError

Decode a QPACK field section that may reference the dynamic table, given max_entries (from the negotiated maximum table capacity). Reads the section prefix, then every field line — static or dynamic, indexed or literal, pre- or post-base — into the header list. Raises if a reference is out of range or the section is malformed.

enum
enum QpackEncoderInst

An encoder-stream instruction (RFC 9204 §4.3).

enum
enum QpackDecoderInst

A decoder-stream instruction (RFC 9204 §4.4).

fn
fn qpack_encode_encoder_inst( inst : QpackEncoderInst, huffman? : Bool = true) -> Bytes

Encode an encoder-stream instruction (RFC 9204 §4.3). huffman chooses whether the literal strings of the insert instructions are Huffman-coded.

fn
fn qpack_decode_encoder_inst( input : BytesView) -> (QpackEncoderInst, Int)? raise QpackError

Decode one encoder-stream instruction from the front of input (RFC 9204 §4.3), dispatching on the pattern in the first byte's high bits. Returns (instruction, bytes-consumed), or None when the instruction has not fully arrived.

fn
fn qpack_encode_decoder_inst(inst : QpackDecoderInst) -> Bytes

Encode a decoder-stream instruction (RFC 9204 §4.4).

fn
fn qpack_decode_decoder_inst(input : BytesView) -> (QpackDecoderInst, Int)?

Decode one decoder-stream instruction from the front of input (RFC 9204 §4.4). Returns (instruction, bytes-consumed), or None on a partial read.

item
extend QpackDecoderInst with Debug::
item
extend QpackDecoderInst with Eq::
item
extend QpackEncoderInst with Debug::
item
extend QpackEncoderInst with Eq::
fn
fn qpack_int_encode(value : Int, prefix_bits : Int, flags : Int) -> Bytes

Encode value as an RFC 7541 §5.1 prefix_bits-prefix integer, OR-ing flags into the high 8 - prefix_bits bits of the first byte (the representation's pattern).

fn
fn qpack_int_decode(input : BytesView, prefix_bits : Int) -> (Int, Int)?

Decode an RFC 7541 §5.1 prefix_bits-prefix integer from the front of input (its first byte holds the value in its low prefix_bits, any flags in the high bits are ignored). Returns (value, bytes-consumed), or None when a continuation byte has not yet arrived (a partial read).

fn
fn qpack_max_entries(max_capacity : Int) -> Int

The number of entries that fit in a table of max_capacity bytes at the 32-byte minimum entry size — MaxEntries in RFC 9204 §4.5.1.1.

fn
fn qpack_encode_section_prefix( req_insert_count : Int, base : Int, max_entries : Int) -> Bytes

Encode a field-section prefix (RFC 9204 §4.5.1): the Required Insert Count transmitted modulo 2*max_entries, then the Base as a sign bit (0 when base >= req_insert_count) plus the delta.

fn
fn qpack_decode_required_insert_count( encoded : Int, max_entries : Int, total_inserts : Int) -> Int raise QpackError

Reconstruct the absolute Required Insert Count from its encoded form, max_entries, and the number of inserts the decoder has processed (RFC 9204 §4.5.1.1).

fn
fn qpack_decode_section_prefix( input : BytesView, max_entries : Int, total_inserts : Int) -> (Int, Int, Int)? raise QpackError

Decode a field-section prefix (RFC 9204 §4.5.1), given max_entries and the decoder's total inserts so far. Returns (req_insert_count, base, bytes-consumed), or None on a partial read.

fn
fn qpack_static_len() -> Int

The number of entries in the QPACK static table (99).

fn
fn qpack_static_get(index : Int) -> (String, String)?

The (name, value) entry at index, or None if index is out of range.

fn
fn qpack_static_find(name : String, value : String) -> Int?

The index of the first entry whose name and value both match, for an indexed field line; None if no entry matches exactly.

fn
fn qpack_static_find_name(name : String) -> Int?

The index of the first entry whose name matches (regardless of value), for a literal field line with a name reference; None if no entry has the name.

§Primitives

The cryptography the transports need: AES and GCM, SHA-1 and SHA-256, HMAC and HKDF, X25519 and ECDSA.

fn
fn aes128_key_schedule(key : Bytes) -> Array[Int]

The 11 AES-128 round keys as 176 flattened bytes (FIPS-197 §5.2 KeyExpansion). Round r occupies bytes 16*r ..< 16*r+16.

fn
fn aes128_encrypt_block(schedule : Array[Int], block : Bytes) -> Bytes

Encrypt one 16-byte block under the expanded key (FIPS-197 §5.1 Cipher). The state is column-major: byte k is row k % 4, column k / 4.

fn
fn aes128_gcm_seal( key : Bytes, nonce : Bytes, plaintext : Bytes, aad : Bytes) -> Bytes

AEAD_AES_128_GCM seal: encrypt plaintext under key and the 12-byte nonce with additional data aad, returning ciphertext || tag (16-byte tag appended).

fn
fn aes128_gcm_open( key : Bytes, nonce : Bytes, packet : Bytes, aad : Bytes) -> Bytes?

AEAD_AES_128_GCM open: verify the trailing 16-byte tag of packet and, on success, return the decrypted plaintext; None if authentication fails. The tag comparison runs over all bytes to avoid a length-dependent early exit.

fn
fn sha1(msg : Bytes) -> Bytes

The SHA-1 digest of msg (20 bytes).

fn
fn sha256(msg : Bytes) -> Bytes

The SHA-256 digest of msg (32 bytes).

fn
fn hmac_sha256(key : Bytes, msg : Bytes) -> Bytes

HMAC-SHA256 of msg under key: H((K0 ^ opad) || H((K0 ^ ipad) || msg)), where K0 is key hashed down to 32 bytes if it exceeds the 64-byte block, then right-padded with zeros to the block size (RFC 2104 §2).

fn
fn hkdf_extract(salt : Bytes, ikm : Bytes) -> Bytes

HKDF-Extract (RFC 5869 §2.2): PRK = HMAC(salt, IKM). An empty salt is replaced by a string of HashLen zero bytes, per the RFC.

fn
fn hkdf_expand(prk : Bytes, info : Bytes, length : Int) -> Bytes

HKDF-Expand (RFC 5869 §2.3): T(i) = HMAC(PRK, T(i-1) || info || i), with the output the concatenation of the T(i) truncated to length. length may not exceed 255 * HashLen (RFC 5869 caps the counter at one byte).

fn
fn hkdf_expand_label( secret : Bytes, label : Bytes, context : Bytes, length : Int) -> Bytes

HKDF-Expand-Label (RFC 8446 §7.1): expand under the structured HkdfLabel struct { uint16 length; opaque label<7..255>; opaque context<0..255>; } where the label is prefixed with "tls13 ". QUIC reuses this verbatim (RFC 9001).

fn
fn x25519(k : Bytes, u : Bytes) -> Bytes

X25519 (RFC 7748 §5): the scalar k applied to u-coordinate u, both 32-byte little-endian, returning the 32-byte little-endian result u-coordinate. The scalar is clamped and the u-coordinate's high bit masked per §5.

fn
fn x25519_base() -> Bytes

The Curve25519 base point u=9 as a 32-byte little-endian scalar input, for turning a private key into a public key: x25519(private, x25519_base()).

struct
struct EcdsaPublicKey

An ECDSA P-256 public key: the curve point (x, y). The ES256 verification key.

fn
fn EcdsaPublicKey::from_hex( x_hex : String, y_hex : String) -> EcdsaPublicKey

Build a P-256 public key from the hex-encoded affine coordinates — e.g. the two halves of openssl's uncompressed pub point after its 04 prefix.

fn
fn EcdsaPublicKey::on_curve(self : EcdsaPublicKey) -> Bool

Whether the key is a point on P-256: y² ≡ x³ - 3x + b (mod p), with both coordinates in the field. A point off the curve is not a key — the group law applied to it computes in a different and much weaker group, which is how an invalid-curve attack extracts a private key from the peer that accepts it.

fn
fn ecdsa_p256_sha256_verify( msg : Bytes, sig : Bytes, key : EcdsaPublicKey) -> Bool

ECDSA P-256 verify with SHA-256 — the ES256 primitive (FIPS 186-4 §6.4.2). sig is the raw r || s (two 32-byte big-endian integers). Returns whether the signature is valid for msg under key: r, s in range, then u1·G + u2·Q has x-coordinate ≡ r (mod n).

struct
struct EcdsaPrivateKey

An ECDSA P-256 private key: the scalar d. The ES256 signing key.

fn
fn EcdsaPrivateKey::from_hex(d_hex : String) -> EcdsaPrivateKey

Build a P-256 private key from its hex-encoded scalar.

fn
fn EcdsaPrivateKey::public_key(self : EcdsaPrivateKey) -> EcdsaPublicKey

The public key Q = d·G for this private key.

fn
fn ecdsa_p256_sha256_sign(msg : Bytes, key : EcdsaPrivateKey) -> Bytes

ECDSA P-256 sign with SHA-256 — the ES256 signing primitive (FIPS 186-4 §6.4.1) with the deterministic nonce of RFC 6979. Returns the raw r || s (two 32-byte big-endian integers). Deterministic, so the same message and key always produce the same signature.