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.
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~, ...) -> 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 -> 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. HTTP/1.1 messages are read and written by moonhttp/http1; what is here is the connection's inbox that keeps a pipelined request from being lost between them.
async fn serve( app : @moonasgi.AsgiApp, host~ : String = "127.0.0.1", port~ : Int32 = @config.port, backlog~ : Int32 = 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.
async fn serve_config( app : @moonasgi.AsgiApp, config : @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 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 WsConfig::new(max_size~ : Int32 = @ws.limit) -> WsConfig
Build a WsConfig, defaulting max_size to uvicorn's own 16 MiB.
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.
let port : Int32 = 12000
The default port for the TCP serving entry points. HTTP/1.1, WebSocket upgrades and HTTP/2 share one listener, told apart by ALPN under TLS and by the HTTP/2 preface in the clear. quic_serve takes an explicit UDP address; it may use the same number because UDP and TCP have separate port spaces.
fn Config::new( host~ : String = "127.0.0.1", port~ : Int32 = port, backlog~ : Int32 = 2048, dual_stack~ : Bool = false, reuse_addr~ : Bool = true, server_headers~ : Map[String, String] = Map([("server", "mooncat")]), max_connections? : Int32, allow_failure~ : Bool = true, graceful_timeout? : Int32, root_path~ : String = "", logger~ : Logger = Logger::new(), timeout_keep_alive~ : Int32 = 5000, timeout_handshake~ : Int32 = 60000, limit_max_requests? : Int32, limit_concurrency? : Int32, max_head_size~ : Int32 = 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 12000, 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 Config::bind(self : Config) -> String
The host:port string used to resolve the listen address.
suberror LifespanErrorRaised when an application reports lifespan.startup.failed or lifespan.shutdown.failed, carrying the failure message the app supplied.
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 Lifespan::new(app : @moonasgi.AsgiApp) -> Lifespan
Create a lifespan driver for app, with empty unbounded message queues and empty lifespan state.
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).
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.
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 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 ShutdownHandle::new() -> ShutdownHandle
Create an idle shutdown handle with empty unbounded signal queues.
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.
async fn ShutdownHandle::request_stop(self : ShutdownHandle) -> Unit
Request a graceful shutdown without waiting for it to finish.
async fn serve_graceful( app : @moonasgi.AsgiApp, config : @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 startup before the listener binds and shutdown on the way out. A single acceptor task spawns one handler task per connection, so requests are served concurrently, each through mooncat's HTTP/1.1 codec and, on an upgrade, its WebSocket bridge. serve, serve_config and serve_tls_config run this same loop; this entry point adds the handle. 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 connection ceilings: 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 binds the port once in the parent (SO_REUSEADDR, an inheritable socket) and hands that socket to N worker processes it starts with multiprocessing's spawn context. moonbitlang/async can start a process but cannot give it a listening socket, 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 the concurrency a single uvicorn worker provides on its event loop. Multiple workers land when the async layer can pass a listening socket to a child process. ## 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.
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 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 and the format are moondate's; what is here is the millisecond clock mooncat reads, brought down to the whole second the header is written to.
fn http_date_now() -> String
The current instant as an HTTP-date, reformatted only when the epoch second has moved on.
fn proxy_rewrite( headers : Map[String, String], client : (String, Int32)?, scheme : String, trusted~ : Array[String], websocket~ : Bool = false) -> ((String, Int32)?, 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.
struct Logger
How much the server says, and where it goes.
fn Logger::new( level~ : @moonlog.Level = Info, access~ : Bool = true, write~ : (String) -> Unit = line => println(line)) -> Logger
A logger printing to stdout at level, with the access log on — uvicorn's defaults. access off silences the per-request lines while keeping the server's own.
fn Logger::silent() -> Logger
A logger that says nothing, for an embedder that does its own reporting or a test that would rather not have output.
fn Logger::enabled(self : Logger, level : @moonlog.Level) -> Bool
Whether a line at level would be written.
fn Logger::log( self : Logger, level : @moonlog.Level, message : String) -> Unit
Write one server-lifecycle line (uvicorn's uvicorn.error logger, which carries ordinary startup messages as well as failures).
fn Logger::access_line( self : Logger, client : String, verb : String, target : String, http_version : String, status : Int32) -> Unit
Write the one-line-per-request access log (uvicorn's uvicorn.access), in the same shape: peer, request line, status.
fn reason(code : Int32) -> String
The reason phrase an access line pairs with a status. Lives here rather than beside the native server because the access line is written on every backend.
§Command line
uvicorn's flags, which are the half of `uvicorn main:app` that carries over to a language that links its app in rather than importing it by string. run(app) is the whole body of a server program; parse(argv) returns the plan without opening anything. A uvicorn flag mooncat cannot honour answers with the reason rather than with 'unexpected argument'.
suberror RefusedWhy a command line was not accepted. Unknown flags and missing values are argparse's to report — it already prints what clap prints. These three are mooncat's own.
extend Refused with Eq::
extend Refused with Debug::
extend Refused with Show::
impl Show for Refused with fn output(self, logger)
struct Plan
What a parsed command line says to do: the configuration, whether to wrap it in TLS, and whether to speak HTTP/2 in the clear. The three together pick which serve_* runs, which is why they travel as one value instead of as three arguments.
fn command(version~ : String = "0.14.6") -> @argparse.Command
The command line, as argparse describes it. Options carry no defaults on purpose: an absent flag must be distinguishable from one that happens to match the default, because everything not given is left as parse found it in base.
fn parse( argv : ArrayView[String], base~ : @mooncat.Config = @mooncat.Config::new()) -> Plan raise
Read a command line into a Plan, starting from base and overriding only what was given. --help and --version are argparse's: it prints and exits the process, as uvicorn and clap do, so they never come back here. One wart worth stating: Logger is opaque, so --log-level and --access-log cannot adjust the one in base — either flag replaces it with a fresh logger at the level asked for, which drops a custom write. Neither flag leaves base's logger alone.
async fn run( app : @moonasgi.AsgiApp, argv? : ArrayView[String], base~ : @mooncat.Config = @mooncat.Config::new()) -> Unit
Serve app the way the command line says to. This is the whole of a server program: ``moonbit nocheck async fn main { @cli.run(app) } ` ` moon run src/main --target native -- --port 9000 --log-level debug ` With no argv, the real one is read and its first element — the path to the executable — is dropped. base` is where the flags start from, so a program that wants its own defaults passes them and lets the command line override only what was typed.
async fn serve(app : @moonasgi.AsgiApp, plan : Plan) -> Unit
Serve under a plan already parsed, for a program that wants to look at the plan — or change it — before the socket opens.
§WebSocket
The frame-to-Event bridge that drives a websocket Scope through the SEAM: connect / accept / receive / send / disconnect / close, over any byte stream, so ws:// and wss:// share it. The RFC 6455 frames and the opening handshake are moonhttp's ws and upgrade packages.
async fn read_frame( reader : &@io.Reader, client~ : Bool = true, limit~ : Int32 = @ws.limit) -> @ws.Frame
One frame off a stream (RFC 6455 §5.2). The header is read in the order the format is laid out — two bytes, then the extended length, then the mask — and @ws.check runs on the header before the payload is read, so an enormous announced length is refused rather than allocated. client is the server's position by default: a client's frames must be masked (§5.1), so reading a server's frames takes client=false. Raises @ws.Refused carrying the status the connection is then failed with, and passes the stream's end through.
async fn read_message( reader : &@io.Reader, writer : &@io.Writer, limit~ : Int32 = @ws.limit) -> @ws.Message
One complete message off a stream, with the control frames between its fragments handled: a ping is answered on writer, a pong is dropped, and a close is echoed before the message comes back as that close. This is the server's receive path, so every frame must arrive masked. The reply frames go out unmasked because a server must not mask (§5.1).
§HTTP/2
Serving HTTP/2, chosen by ALPN under TLS and by the connection preface in the clear: the frame pump, stream multiplexing and the send windows, each stream driving the app as one request. The frames are moonhttp/http2 and the header compression moonhttp/hpack.
async fn serve_h2c( app : @moonasgi.AsgiApp, host~ : String = "127.0.0.1", port~ : Int32 = @config.port) -> Unit
Serve a moonasgi ASGI application over **HTTP/2 cleartext (h2c)** only — the prior-knowledge, no-TLS HTTP/2 profile (RFC 9113 §3.3). Convenience wrapper over serve_h2c_config that builds a Config from host / port. Blocks in the accept loop until the running task is cancelled. serve and serve_tls speak HTTP/2 as well, beside HTTP/1.1 on the same port; this entry point is for a listener that is to speak nothing else.
async fn serve_h2c_config( app : @moonasgi.AsgiApp, config : @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_h2.
§TLS
HTTPS: the certificate chain and key as PEM files, the same on every platform. The handshake is moontls on a moonnet connection, offering h2 and http/1.1 by ALPN; after it a connection is served like a plain one, WebSocket upgrades included.
struct TlsCert
The certificate chain and private key an HTTPS server presents (← uvicorn's ssl_certfile and ssl_keyfile), as PEM files. The chain file holds the leaf first, then any intermediates; the key file holds a PRIVATE KEY, EC PRIVATE KEY or RSA PRIVATE KEY. The same files serve on every platform: the TLS is moontls, not the system's.
let alpn : Array[String] = ["h2", "http/1.1"]
The application protocols a TLS listener offers, in preference order: HTTP/2 first, as hypercorn and nginx do, falling back to HTTP/1.1.
async fn TlsCert::load( self : TlsCert, alpn~ : Array[String] = alpn) -> @tls.Server
Read the files into a TLS server configuration. A key that is not the leaf's is refused here, at startup, rather than by every client's handshake.
async fn serve_tls( app : @moonasgi.AsgiApp, certificate_file~ : String, private_key_file~ : String, host~ : String = "127.0.0.1", port~ : Int32 = @config.port, backlog~ : Int32 = 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 serves HTTP/2 when ALPN settles on h2, HTTP/1.1 otherwise. A WebSocket upgrade on an HTTP/1.1 connection is served too (wss://): the 101, the subprotocol echo and the frames all travel over the encrypted stream.
async fn serve_tls_config( app : @moonasgi.AsgiApp, config : @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 completes a TLS handshake within timeout_handshake and then runs the same loop as a plain one.
§QUIC
The UDP half, in a package of its own because of what it carries: QUIC performs its own TLS 1.3 handshake rather than riding a record layer, so this is the only part of mooncat that needs moonquic, moontls and mooncred. The socket, the datagram loop that routes by connection ID, the timers, and the handshake crypto. QUIC itself is moonquic's.
fn 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.
async fn serve_endpoint( ep : Endpoint, server : @conn.Endpoint[@socket.Addr], tick_ms~ : Int32 = 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.
async fn serve( addr : String, server : @conn.Endpoint[@socket.Addr], tick_ms~ : Int32 = 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 Endpoint
A QUIC UDP endpoint: a bound datagram socket.
async fn Endpoint::bind(addr : String) -> Endpoint
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 Endpoint::local_addr(self : Endpoint) -> @socket.Addr
The address the endpoint is bound to (the concrete port when bound to port 0).
async fn Endpoint::recv_datagram( self : Endpoint, max~ : Int32 = 2048) -> (Bytes, @socket.Addr)
Receive the next datagram and its sender's address, copying up to max bytes.
async fn Endpoint::send_datagram( self : Endpoint, data : Bytes, to : @socket.Addr) -> Unit
Send data as a single datagram to to.
fn Endpoint::close(self : Endpoint) -> Unit
Close the endpoint's socket.
fn tls13_x25519_public(private_key : Bytes) -> Bytes
The ephemeral x25519 public key for a private scalar (RFC 7748 §5).
fn tls13_ecdhe_shared(private_key : Bytes, peer_public : Bytes) -> Bytes
The x25519 shared secret with a peer's public key. A low-order peer share makes the secret all zeroes, which mooncrypt refuses; here that becomes an empty answer, because the handshake's response to it is an alert rather than a crash.
fn tls13_handshake_secret_from_ecdhe( private_key : Bytes, peer_public : Bytes) -> Bytes
The Handshake Secret from our scalar and the peer's key share: run the ECDHE, then RFC 8446 §7.1's ladder.
fn tls13_server_hs_secret_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> Bytes?
A server's handshake traffic secret from the ClientHello it received: pull the client's x25519 key_share and run the ECDHE with server_private over the ClientHello..ServerHello transcript. None if the message is not a ClientHello or offers no x25519 share — the case a server answers with a HelloRetryRequest rather than a secret.
fn tls13_certificate_verify_sign( key : @ecdsa.PrivateKey, context : String, transcript_hash : Bytes) -> Bytes
Sign a CertificateVerify over the transcript with an ES256 key (RFC 8446 §4.4.3): the message body, its scheme and signature framed by moontls, over the signed content moontls builds.
fn tls13_certificate_verify_check( key : @ecdsa.PublicKey, context : String, transcript_hash : Bytes, body : Bytes) -> Bool
Check a CertificateVerify body against the peer's ES256 public key. False on a scheme this server does not verify, a malformed body, or a signature that does not check out — one answer for every way of being wrong, because which way it was wrong is not the peer's business.
fn tls13_quic_encrypted_extensions( alpn : String, params : Array[@conn.Param]) -> Bytes
A QUIC server's EncryptedExtensions body: the negotiated ALPN protocol and the server's transport parameters. RFC 9001 §8.2 makes quic_transport_parameters mandatory for a QUIC server, and the parameters themselves are QUIC's — moontls carries the extension without reading it, so encoding them is this side's job.
fn tls_hello_quic_transport_params( extensions : Array[@ext.Ext]) -> Array[@conn.Param]
The QUIC transport parameters an extension list carries, in order. Empty when the extension is absent or its block is truncated: a QUIC endpoint treats a missing block as no parameters and falls back to the defaults RFC 9000 §18.2 gives.
fn tls13_ecdhe_handshake_traffic_secret( private_key : Bytes, peer_public : Bytes, transcript_hash : Bytes, is_client : Bool) -> Bytes
A handshake traffic secret straight from an ECDHE exchange and a transcript. is_client picks which side's secret: the two are derived from the same handshake secret under different labels (RFC 8446 §7.1), so one call serves both ends of a test and both ends of a connection.
fn tls13_client_hello_x25519(client_hello : Bytes) -> Bytes?
The x25519 public key a ClientHello's key_share offers, or None when it carries none for that group.
fn tls13_ecdhe_secrets_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> (Bytes, Bytes)?
Both handshake traffic secrets from a ClientHello's key share: the server's and the client's, in that order. None when the ClientHello offers no x25519 share, which is the one case a server has to answer with a HelloRetryRequest rather than a secret.
fn tls_server_hello_key_share(sh : @msg.Server) -> (Int32, Bytes)?
The group and key a ServerHello's key_share selected.
fn tls_selected_alpn(extensions : Array[@ext.Ext]) -> String?
The single ALPN protocol a server's extension list selected, or None.
fn ec_private(scalar : String) -> @ecdsa.PrivateKey
A P-256 private key from the hex of its scalar. Hex is what a fixture and an example carry. A key from a file is PEM or DER, which is mooncrypt/key's to read when that package lands; until then a deployment reads its own file and hands the scalar in.