Server
~import orisha/indexOrisha — the contract.
lib/index.k · 26 tors
Orisha — the contract. · 40 more lines
Orisha — the contract.
Everything Orisha declares lives here, and nothing in this file is Zig, so
there is no parser to switch out of and not a single `~` anywhere. The Zig
that implements it — the socket calls, the request parser, the router
transform — sits in the companion `index.kz` beside this file. Same stem,
same module: the compiler merges them.
That split is the whole reason this file reads the way it does. These
declarations used to live inside 1,056 lines of Zig, which forced a `~` onto
every one of them; the character was never about the Koru, it was about the
host language it was sharing a file with.
Public API:
orisha:serve(port) - Start server (calls abstract handler for each request)
orisha:handler - Abstract event users implement for request handling
orisha:router(req) - Pattern-branch router transform
orisha:static(name, root, ...) - Declare static files to embed at compile time
orisha:static-router(name) - Transform: inline static file lookup + fallback
orisha:header(req, name) - One request header, scanned lazily -> ?string
orisha:cookie(req, name) - One pair out of the Cookie header -> ?string
orisha:form(req, name) - One urlencoded field out of req.body -> ?string
orisha:query-param(req, name) - The same codec over req.query -> ?string
Response.extra_headers - CRLF block of response headers (Location,
Set-Cookie, ...) spliced into the head
Example (simplest server):
~orisha:handler -> { status: 200, body: "Hello!", content_type: "text/plain" }
~orisha:serve(port: 3001)
Example (API + static):
~orisha:handler = orisha:router(req)
! [GET /api/health] -> { status: 200, body: "ok", content_type: "application/json" }
! [*] |> orisha:static-router(name: "site")
Example (pure static site):
~import orisha
~orisha:static(name: "site", root: "build", fallback: "200.html")
~orisha:handler = orisha:static-router(name: "site")
~orisha:serve(port: 3000)
listen
lib/index.k:54// LISTEN - Start listening on a port
// `*ServerHandle`, `*ConnHandle` and `*Request` are Zig types declared in the
// companion. A contract may NAME a host type it does not itself declare —
// what it may not do is contain host code.
~pub tor listen { port: u16 }
| listening *ServerHandle
| failed stringaccept
lib/index.k:62// ACCEPT - Accept a connection and read the request
~pub tor accept { server: *ServerHandle }
| request { req: *Request, conn: *ConnHandle, server: *ServerHandle }
| failed { msg: string, server: *ServerHandle }send
lib/index.k:70// SEND - Send HTTP response and close connection
~pub tor send {
conn: *ConnHandle,
status: u16,
body: string,
content_type: ?string
}
| sent
| failed string// HANDLER - Abstract event for request handling
// Users implement this to handle requests. Default returns 404.
//
// @retain: handler_event is referenced from the serve proc's raw Zig via
// $mod.handler_event.handler(...) — invisible to the dead-stripper.
~[abstract|retain] pub tor handler { req: *Request } -> Response// PRE-REQUEST DISPATCH — routes whose answer never needed the Request.
// `! [GET /x] -> { ... }` literal arms produce a fixed Response by
// construction: no `req` exists in their scope, so they are provably
// request-free. The router transform collects those arms into this impl —
// answer() consults it BEFORE materializing Request/head fields, and a hit
// skips all of that machinery. Miss (null) falls through to the full
// handler unchanged. Default: nothing is request-free.
//
// @retain: handler_lite_event is referenced from answer's raw Zig via
// $mod.handler_lite_event.handler(...) — invisible to the dead-stripper.
~[abstract|retain] pub tor handler-lite {
method: string,
path: string
} -> ?Response~[abstract] pub tor init {}
| ready
| failed stringserve
lib/index.k:136// SERVE — a Koru flow over a platform pump
// `serve` used to be two ~200-line Zig bodies, one per readiness mechanism,
// that differed in about twenty lines and duplicated everything else. The loop
// now lives in orisha/pump, one variant per platform; what remains here is the
// part that is the same everywhere, and it is a flow.
//
// Adding a platform is a `~proc run|<name>` in orisha/pump plus a line in the
// variants block. Nothing in this file changes.
~pub tor serve { port: u16, tls_port: ?u16, cert_path: ?string, key_path: ?string }
| shutdown string
| failed stringanswer
lib/index.k:162// ANSWER — parse one request, run the handler, shape the response
// Platform-free by construction: it takes the exchange the pump dispatched
// and returns response bytes — never a platform handle type. Every pump
// shares it. The body is in the companion.
//
// It reads `x.raw` in place rather than `pump:raw`'s arena copy, and the
// parse `scanRequest` already produced rather than re-scanning the head —
// one request, one walk.
~pub tor answer { x: orisha/pump:*Exchange } -> { head: string, body: string, body_allocated: bool, date_hole: usize, close: bool, head_static: bool, body_static: bool, delay_ms: i64 }release-body
lib/index.k:166// Free a handler body that fmt.blk allocated. Static slices and fmt:ln TLS
// buffers arrive with allocated=false; those must not be freed.
~pub tor release-body { body: string, allocated: bool }serve-file
lib/index.k:176// Read one file off disk and answer it — the runtime half of what
// `orisha:static` does at compile time. The file is read on EVERY request:
// the HttpArena static profiles rewrite their tree mid-benchmark and a
// cached body fails the staleness probe by construction. Pre-compressed
// `<file>.br` / `<file>.gz` siblings on disk are served when the client's
// Accept-Encoding allows, strongest first; `root` comes from STATIC_DIR
// (default /data/static, the harness's mount). Traversal past the root is
// refused with 404 — `path` is attacker-shaped input, not a filename hint.
~pub tor serve-file { req: *Request, path: string } -> orisha:Responseint-body
lib/index.k:183// A decimal i64 rendered into threadlocal scratch — the zero-alloc body
// producer for integer answers (baseline, delay). The slice is valid until
// the next int-body call on this worker; `reply` stages (copies) it
// synchronously, so it rides out as `body_static` — never allocated,
// never freed.
~pub tor int-body { n: i64 } -> stringgrpc-sum-reply
lib/index.k:188// gRPC unary GetSum codec: the length-prefixed SumRequest frame in, the
// SumReply frame out. Threadlocal scratch, body_static — same discipline
// as int-body.
~pub tor grpc-sum-reply { body: ?string } -> stringheader
lib/index.k:203// REQUEST HEADERS — lazy reads over the raw block
// `req.headers` is the whole header block — request line through the blank
// line — borrowed from the request bytes. These accessors scan it only when
// asked, so a route that never asks pays one stored slice and nothing more.
// Names are case-insensitive (RFC 9110); the answer borrows the request and
// dies with it. `Cookie`, `Origin`, `Sec-WebSocket-*` — everything the client
// sent is reachable this way.
//
// orisha:header(req: req, name: "origin")
// orisha:header(req: req, name: "sec-websocket-key")
~pub tor header { req: *Request, name: string } -> ?stringform
lib/index.k:219// BODY FIELDS — one codec, two seams
// `form` reads application/x-www-form-urlencoded fields out of `req.body` and
// refuses (null) a request that never declared that content type — parsing a
// JSON body as pairs is how a string value becomes a phantom field.
// `query-param` runs the same codec over `req.query`, where no declaration
// exists. `+` is a space, %XX a byte; a pair without `=` is a flag and
// matches no name.
~pub tor form { req: *Request, name: string } -> ?stringquery-param
lib/index.k:220~pub tor query-param { req: *Request, name: string } -> ?stringbody
lib/index.k:227// The raw spans the parsed request carried — `body` is the entity bytes
// exactly as received (no codec, no content-type gate) and `query` is the
// unparsed query string. Handlers that need more than one field — or none
// of the named-param codec — read the span and parse it themselves. Both
// borrow the request and die with it.
~pub tor body { req: *Request } -> ?stringquery
lib/index.k:228~pub tor query { req: *Request } -> ?stringform-ids
lib/index.k:237// Every value `name` carries in the form body, decimal-parsed and joined
// "," — Rails' `params[:user_ids]` shape for multi-select fields. Non-
// numeric values drop out; no match at all answers "0" so the string can
// sit inside a SQL IN () list and still exclude everything.
// Spelling ruled (campfire port, 2026-10-17): `form-ids` sits on the
// request-reading surface beside `form`/`query-param` — same req+name
// shape, contract answered as a string for the SQL use-site.
~pub tor form-ids { req: *Request, name: string } -> stringraw-body
lib/index.k:246// The undecoded request body, or "". The bot-messages endpoint reads its
// payload this way (Messages::ByBotsController's raw_request_body) — no
// content-type gate, matching request.body.read. `raw-body` differs from
// `body` only in the nullable contract: it collapses absence to "" where
// `body` answers null.
// Spelling ruled (same ruling): `raw-body` is the un-decoded-body reader;
// absent body answers "" as a designed answer, not an error path.
~pub tor raw-body { req: *Request } -> string// The descriptor the request arrived on — `pump:fileno`'s handler-side
// twin. A route that upgrades can register the connection for `ws-send-fd`
// pushes, and `! closed` reports the same integer when it goes away.
~pub tor fd { req: *Request } -> usizews-accept
lib/index.k:262// WS-ACCEPT — the RFC 6455 handshake value
// base64(SHA-1(`Sec-WebSocket-Key` + the fixed GUID)) — the one part of an
// upgrade a handler cannot spell itself. Null when the request carries no
// key. The answer is a `status: 101` Response whose `extra_headers` spell
// `Upgrade`/`Connection: Upgrade`/`Sec-WebSocket-Accept`; `answer` emits the
// 101 head and the pump flips the connection to frames on the way out.
~pub tor ws-accept { req: *Request } -> ?stringgeneration-bump
lib/index.k:273// GENERATION-BUMP — the page cache's invalidation signal
// A GET 200 whose Response sets `cacheable` is stored post-render (keyed on
// method + target + cookie + host + negotiated encoding) and hits skip
// dispatch. One bump retires every stored page — call it from the write
// paths the pages must reflect. Pages never decide this themselves: the
// app knows which writes matter, the cache only keeps the ledger.
~pub tor generation-bump {}~[comptime|transform] pub tor router {
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult// STATIC - Declare files to embed at compile time
// Data-only event.
// - name: identifier for referencing via static-router
// - root: directory to embed (relative to the consuming file)
// - fallback: optional file for SPA fallback (relative to root, e.g. "200.html")
// - gzip_only: bake one representation per compressible route (the gzip one,
// served to every client) instead of the negotiated identity+gzip pair —
// halves the embed for hosts with an artifact size cap (Vercel, 2026-10-15)
~[comptime] pub tor static {
name: string,
root: string,
fallback: ?string,
gzip_only: ?bool
}~[comptime|transform] pub tor static-router {
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult